diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 37587868da58..acb7b869b269 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -972,18 +972,16 @@ object Parsers { followedByToken(LARROW) // `<-` comes before possible statement starts } - /** Are the next token the "GivenSig" part of a given definition, - * i.e. an identifier followed by type and value parameters, followed by `:`? + /** Are the next tokens a valid continuation of a named given def? + * i.e. an identifier, possibly followed by type and value parameters, followed by `:`? * @pre The current token is an identifier */ - def followingIsOldStyleGivenSig() = + def followingIsGivenDefWithColon() = val lookahead = in.LookaheadScanner() if lookahead.isIdent then lookahead.nextToken() - var paramsSeen = false def skipParams(): Unit = if lookahead.token == LPAREN || lookahead.token == LBRACKET then - paramsSeen = true lookahead.skipParens() skipParams() else if lookahead.isNewLine then @@ -1002,6 +1000,11 @@ object Parsers { } } + def followingIsArrow() = + val lookahead = in.LookaheadScanner() + lookahead.skipParens() + lookahead.token == ARROW + def followingIsExtension() = val next = in.lookahead.token next == LBRACKET || next == LPAREN @@ -3441,7 +3444,11 @@ object Parsers { /** ContextTypes ::= FunArgType {‘,’ FunArgType} */ def contextTypes(paramOwner: ParamOwner, numLeadParams: Int, impliedMods: Modifiers): List[ValDef] = - val tps = commaSeparated(() => paramTypeOf(() => toplevelTyp())) + typesToParams( + commaSeparated(() => paramTypeOf(() => toplevelTyp())), + paramOwner, numLeadParams, impliedMods) + + def typesToParams(tps: List[Tree], paramOwner: ParamOwner, numLeadParams: Int, impliedMods: Modifiers): List[ValDef] = var counter = numLeadParams def nextIdx = { counter += 1; counter } val paramFlags = if paramOwner.isClass then LocalParamAccessor else Param @@ -3468,18 +3475,20 @@ object Parsers { def termParamClause( paramOwner: ParamOwner, numLeadParams: Int, // number of parameters preceding this clause - firstClause: Boolean = false // clause is the first in regular list of clauses + firstClause: Boolean = false, // clause is the first in regular list of clauses + initialMods: Modifiers = EmptyModifiers ): List[ValDef] = { - var impliedMods: Modifiers = EmptyModifiers + var impliedMods: Modifiers = initialMods def addParamMod(mod: () => Mod) = impliedMods = addMod(impliedMods, atSpan(in.skipToken()) { mod() }) def paramMods() = if in.token == IMPLICIT then addParamMod(() => Mod.Implicit()) - else - if isIdent(nme.using) then - addParamMod(() => Mod.Given()) + else if isIdent(nme.using) then + if initialMods.is(Given) then + syntaxError(em"`using` is already implied here, should not be given explicitly", in.offset) + addParamMod(() => Mod.Given()) def param(): ValDef = { val start = in.offset @@ -4144,18 +4153,67 @@ object Parsers { * OldGivenSig ::= [id] [DefTypeParamClause] {UsingParamClauses} ‘:’ * StructuralInstance ::= ConstrApp {‘with’ ConstrApp} [‘with’ WithTemplateBody] * - * NewGivenDef ::= [GivenConditional '=>'] NewGivenSig - * GivenConditional ::= [DefTypeParamClause | UsingParamClause] {UsingParamClause} - * NewGivenSig ::= GivenType ['as' id] ([‘=’ Expr] | TemplateBody) - * | ConstrApps ['as' id] TemplateBody - * + * NewGivenDef ::= [id ':'] GivenSig + * GivenSig ::= GivenImpl + * | '(' ')' '=>' GivenImpl + * | GivenConditional '=>' GivenSig + * GivenImpl ::= GivenType ([‘=’ Expr] | TemplateBody) + * | ConstrApps TemplateBody + * GivenConditional ::= DefTypeParamClause + * | DefTermParamClause + * | '(' FunArgTypes ')' + * | GivenType * GivenType ::= AnnotType1 {id [nl] AnnotType1} */ def givenDef(start: Offset, mods: Modifiers, givenMod: Mod) = atSpan(start, nameStart) { var mods1 = addMod(mods, givenMod) val nameStart = in.offset - var name = if isIdent && followingIsOldStyleGivenSig() then ident() else EmptyTermName var newSyntaxAllowed = in.featureEnabled(Feature.modularity) + val hasEmbeddedColon = !in.isColon && followingIsGivenDefWithColon() + val name = if isIdent && hasEmbeddedColon then ident() else EmptyTermName + + def implemented(): List[Tree] = + if isSimpleLiteral then + rejectWildcardType(annotType()) :: Nil + else constrApp() match + case parent: Apply => parent :: moreConstrApps() + case parent if in.isIdent && newSyntaxAllowed => + infixTypeRest(parent, _ => annotType1()) :: Nil + case parent => parent :: moreConstrApps() + + // The term parameters and parent references */ + def newTermParamssAndParents(numLeadParams: Int): (List[List[ValDef]], List[Tree]) = + if in.token == LPAREN && followingIsArrow() then + val params = + if in.lookahead.token == RPAREN && numLeadParams == 0 then + in.nextToken() + in.nextToken() + Nil + else + termParamClause( + ParamOwner.Given, numLeadParams, firstClause = true, initialMods = Modifiers(Given)) + accept(ARROW) + if params.isEmpty then (params :: Nil, implemented()) + else + val (paramss, parents) = newTermParamssAndParents(numLeadParams + params.length) + (params :: paramss, parents) + else + val parents = implemented() + if in.token == ARROW && parents.length == 1 && parents.head.isType then + in.nextToken() + val (paramss, parents1) = newTermParamssAndParents(numLeadParams + parents.length) + (typesToParams(parents, ParamOwner.Given, numLeadParams, Modifiers(Given)) :: paramss, parents1) + else + (Nil, parents) + + /** Type parameters, term parameters and parent clauses */ + def newSignature(): (List[TypeDef], (List[List[ValDef]], List[Tree])) = + val tparams = + if in.token == LBRACKET then + try typeParamClause(ParamOwner.Given) + finally accept(ARROW) + else Nil + (tparams, newTermParamssAndParents(numLeadParams = 0)) def moreConstrApps() = if newSyntaxAllowed && in.token == COMMA then @@ -4176,47 +4234,49 @@ object Parsers { .asInstanceOf[List[ParamClause]] val gdef = - val tparams = typeParamClauseOpt(ParamOwner.Given) - newLineOpt() - val vparamss = - if in.token == LPAREN && (in.lookahead.isIdent(nme.using) || name != EmptyTermName) - then termParamClauses(ParamOwner.Given) - else Nil - newLinesOpt() - val noParams = tparams.isEmpty && vparamss.isEmpty - val hasParamsOrId = !name.isEmpty || !noParams - if hasParamsOrId then - if in.isColon then - newSyntaxAllowed = false + val (tparams, (vparamss0, parents)) = + if in.isColon && !name.isEmpty then in.nextToken() - else if newSyntaxAllowed then accept(ARROW) - else acceptColon() - val parents = - if isSimpleLiteral then - rejectWildcardType(annotType()) :: Nil - else constrApp() match - case parent: Apply => parent :: moreConstrApps() - case parent if in.isIdent && newSyntaxAllowed => - infixTypeRest(parent, _ => annotType1()) :: Nil - case parent => parent :: moreConstrApps() - if newSyntaxAllowed && in.isIdent(nme.as) then - in.nextToken() - name = ident() - + newSignature() + else if hasEmbeddedColon then + newSyntaxAllowed = false + val tparamsOld = typeParamClauseOpt(ParamOwner.Given) + newLineOpt() + val vparamssOld = + if in.token == LPAREN && (in.lookahead.isIdent(nme.using) || name != EmptyTermName) + then termParamClauses(ParamOwner.Given) + else Nil + acceptColon() + (tparamsOld, (vparamssOld, implemented())) + else + newSignature() + val hasParams = tparams.nonEmpty || vparamss0.nonEmpty + val vparamss = vparamss0 match + case Nil :: Nil => Nil + case _ => vparamss0 val parentsIsType = parents.length == 1 && parents.head.isType if in.token == EQUALS && parentsIsType then // given alias accept(EQUALS) mods1 |= Final - if noParams && !mods.is(Inline) then + if !hasParams && !mods.is(Inline) then mods1 |= Lazy ValDef(name, parents.head, subExpr()) else DefDef(name, adjustDefParams(joinParams(tparams, vparamss)), parents.head, subExpr()) - else if (isStatSep || isStatSeqEnd) && parentsIsType && !newSyntaxAllowed then + else if (isStatSep || isStatSeqEnd) && parentsIsType + && !(name.isEmpty && newSyntaxAllowed) + // under new syntax, anonymous givens are translated to concrete classes, + // so it's treated as a structural instance. + then // old-style abstract given if name.isEmpty then - syntaxError(em"anonymous given cannot be abstract") + syntaxError(em"Anonymous given cannot be abstract, or maybe you want to define a concrete given and are missing a `()` argument?", in.lastOffset) + if newSyntaxAllowed then + warning( + em"""This defines an abstract given, which is deprecated. Use a `deferred` given instead. + |Or, if you intend to define a concrete given, follow the type with `()` arguments.""", + in.lastOffset) DefDef(name, adjustDefParams(joinParams(tparams, vparamss)), parents.head, EmptyTree) else // structural instance @@ -4228,12 +4288,16 @@ object Parsers { val templ = if isStatSep || isStatSeqEnd then Template(constr, parents, Nil, EmptyValDef, Nil) - else if !newSyntaxAllowed || in.token == WITH then + else if !newSyntaxAllowed + || in.token == WITH && tparams.isEmpty && vparamss.isEmpty + // if new syntax is still allowed and there are parameters, they mist be new style conditions, + // so old with-style syntax would not be allowed. + then withTemplate(constr, parents) else possibleTemplateStart() templateBodyOpt(constr, parents, Nil) - if noParams && !mods.is(Inline) then ModuleDef(name, templ) + if !hasParams && !mods.is(Inline) then ModuleDef(name, templ) else TypeDef(name.toTypeName, templ) end gdef finalizeDef(gdef, mods1, start) diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 1036397eed7b..a5e1427998bc 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -471,10 +471,16 @@ ConstrMods ::= {Annotation} [AccessModifier] ObjectDef ::= id [Template] ModuleDef(mods, name, template) // no constructor EnumDef ::= id ClassConstr InheritClauses EnumBody -GivenDef ::= [GivenConditional '=>'] GivenSig -GivenConditional ::= [DefTypeParamClause | UsingParamClause] {UsingParamClause} -GivenSig ::= GivenType ['as' id] ([‘=’ Expr] | TemplateBody) - | ConstrApps ['as' id] TemplateBody +GivenDef ::= [id ':'] GivenSig +GivenSig ::= GivenImpl + | '(' ')' '=>' GivenImpl + | GivenConditional '=>' GivenSig +GivenImpl ::= GivenType ([‘=’ Expr] | TemplateBody) + | ConstrApps TemplateBody +GivenConditional ::= DefTypeParamClause + | DefTermParamClause + | '(' FunArgTypes ')' + | GivenType GivenType ::= AnnotType1 {id [nl] AnnotType1} Extension ::= ‘extension’ [DefTypeParamClause] {UsingParamClause} diff --git a/docs/_docs/reference/experimental/typeclasses.md b/docs/_docs/reference/experimental/typeclasses.md index a78e764bbe7d..08839ffe58eb 100644 --- a/docs/_docs/reference/experimental/typeclasses.md +++ b/docs/_docs/reference/experimental/typeclasses.md @@ -329,6 +329,41 @@ The using clause in class `SortedSet` provides an implementation for the deferre **Alternative:** It was suggested that we use a modifier for a deferred given instead of a `= deferred`. Something like `deferred given C[T]`. But a modifier does not suggest the concept that a deferred given will be implemented automatically in subclasses unless an explicit definition is written. In a sense, we can see `= deferred` as the invocation of a magic macro that is provided by the compiler. So from a user's point of view a given with `deferred` right hand side is not abstract. It is a concrete definition where the compiler will provide the correct implementation. +### Abolish Abstract Givens + +With `deferred` givens there is no need anymore to also define abstract givens. The two mechanisms are very similar, but the user experience for +deferred givens is generally more ergonomic. Abstract givens also are uncomfortably close to concrete class instances. Their syntax clashes +with the quite common case where we want to establish a given without any nested definitions. For instance, consider a given that constructs a type tag: +```scala +class Tag[T] +``` +Then this works: +```scala +given Tag[String]() +given Tag[String] with {} +``` +But the following more natural syntax fails: +```scala +given Tag[String] +``` +The last line gives a rather cryptic error: +``` +1 |given Tag[String] + | ^ + | anonymous given cannot be abstract +``` +The underlying problem is that abstract givens are very rare (and should become completely unnecessary once deferred givens are introduced), yet occupy a syntax that looks very close to the more common case of concrete +typeclasses without nested definitions. + +**Proposal:** In the future, let the `= deferred` mechanism be the only way to deliver the functionality of abstract givens. Deprecate the current version of abstract givens, and remove them in a future Scala version. + +**Benefits:** + + - Simplification of the language since a feature is dropped + - Eliminate non-obvious and misleading syntax. + +The only downside is that deferred givens are restricted to be used in traits, whereas abstract givens are also allowed in abstract classes. But I would be surprised if actual code relied on that difference, and such code could in any case be easily rewritten to accommodate the restriction. + ## New Given Syntax A good language syntax is like a Bach fugue: A small set of motifs is combined in a multitude of harmonic ways. Dissonances and irregularities should be avoided. @@ -340,106 +375,185 @@ given [A](using Ord[A]): Ord[List[A]] with ``` The `:` feels utterly foreign in this position. It's definitely not a type ascription, so what is its role? Just as bad is the trailing `with`. Everywhere else we use braces or trailing `:` to start a scope of nested definitions, so the need of `with` sticks out like a sore thumb. -We arrived at that syntax not because of a flight of fancy but because even after trying for about a year to find other solutions it seemed like the least bad alternative. The awkwardness of the given syntax arose because we insisted that givens could be named or anonymous, with the default on anonymous, that we would not use underscore for an anonymous given, and that the name, if present, had to come first, and have the form `name [parameters] :`. In retrospect, that last requirement showed a lack of creativity on our part. - Sometimes unconventional syntax grows on you and becomes natural after a while. But here it was unfortunately the opposite. The longer I used given definitions in this style the more awkward they felt, in particular since the rest of the language seemed so much better put together by comparison. And I believe many others agree with me on this. Since the current syntax is unnatural and esoteric, this means it's difficult to discover and very foreign even after that. This makes it much harder to learn and apply givens than it need be. -Things become much simpler if we introduce the optional name instead with an `as name` clause at the end, just like we did for context bounds. We can then use a more intuitive syntax for givens like this: +The previous conditional given syntax was inspired from method definitions. If we add the optional name to the previous example, we obtain something akin to an implicit method in Scala 2: ```scala -given String is Ord: - def compare(x: String, y: String) = ... - -given [A : Ord] => List[A] is Ord: +given listOrd[A](using Ord[A]): Ord[List[A]] with def compare(x: List[A], y: List[A]) = ... - -given Int is Monoid: - extension (x: Int) def combine(y: Int) = x + y - def unit = 0 ``` -Here, the second given can be read as if `A` is an `Ord` then `List[A]` is also an`Ord`. Or: for all `A: Ord`, `List[A]` is `Ord`. The arrow can be seen as an implication, note also the analogy to pattern matching syntax. +The anonymous syntax was then obtained by simply dropping the name. +But without a name, the syntax looks weird and inconsistent. + +This is a problem since at least for typeclasses, anonymous givens should be the norm. +Givens are like extends clauses. We state a _fact_, that a +type implements a type class, or that a value can be used implicitly. We don't need a name for that fact. It's analogous to extends clauses, where we state that a class is a subclass of some other class or trait. We would not think it useful to name an extends clause, it's simply a fact that is stated. +It's also telling that every other language that defines type classes uses anonymous syntax. Somehow, nobody ever found it necessary to name these instances. -If explicit names are desired, we add them with `as` clauses: +A more intuitive and in my opinion cleaner alternative is to decree that a given should always look like it _implements a type_. Conditional givens should look like they implement function types. The `Ord` typeclass instances for `Int` and `List` would then look like this: ```scala -given String is Ord as intOrd: +given Ord[String]: def compare(x: String, y: String) = ... -given [A : Ord] => List[A] is Ord as listOrd: +given [A : Ord] => Ord[List[A]]: def compare(x: List[A], y: List[A]) = ... - -given Int is Monoid as intMonoid: - extension (x: Int) def combine(y: Int) = x + y - def unit = 0 ``` +The second, conditional instance looks like it implements the function type +```scala +[A : Ord] => Ord[List[A]] +``` +Another way to see this is as an implication: +If `A` is a type that is `Ord`, then `List[A]` is `Ord` (and the rest of the given clause gives the implementation that makes it so). +Equivalently, `A` is `Ord` _implies_ `List[A]` is `Ord`, hence the `=>`. -The underlying principles are: +Yet another related meaning is that the given clause establishes a _context function_ of type `[A: Ord] ?=> Ord[List[A]]` that is automatically applied to evidence arguments of type `Ord[A]` and that yields instances of type `Ord[List[A]]`. Since givens are in any case applied automatically to all their arguments, we don't need to specify that separately with `?=>`, a simple `=>` arrow is sufficiently clear and is easier to read. - - A `given` clause consists of the following elements: +All these viewpoints are equivalent, in a deep sense. This is exactly the Curry Howard isomorphism, which equates function types and implications. - - An optional _precondition_, which introduces type parameters and/or using clauses and which ends in `=>`, - - the implemented _type_, - - an optional name binding using `as`, - - an implementation which consists of either an `=` and an expression, - or a template body. +In the new syntax, a `given` clause consists of the following elements: - - Since there is no longer a middle `:` separating name and parameters from the implemented type, we can use a `:` to start the class body without looking unnatural, as is done everywhere else. That eliminates the special case where `with` was used before. + - An optional name binding `id :` + - Zero or more _conditions_, which introduce type or value parameters. Each precondition ends in a `=>`. + - the implemented _type_, + - an implementation which consists of either an `=` and an expression, + or a template body. -This will be a fairly significant change to the given syntax. I believe there's still a possibility to do this. Not so much code has migrated to new style givens yet, and code that was written can be changed fairly easily. Specifically, there are about a 900K definitions of `implicit def`s -in Scala code on Github and about 10K definitions of `given ... with`. So about 1% of all code uses the Scala 3 syntax, which would have to be changed again. +**Examples:** -Changing something introduced just recently in Scala 3 is not fun, -but I believe these adjustments are preferable to let bad syntax -sit there and fester. The cost of changing should be amortized by improved developer experience over time, and better syntax would also help in migrating Scala 2 style implicits to Scala 3. But we should do it quickly before a lot more code -starts migrating. +Here is an enumeration of common forms of given definitions in the new syntax. We show the following use cases: -Migration to the new syntax is straightforward, and can be supported by automatic rewrites. For a transition period we can support both the old and the new syntax. It would be a good idea to backport the new given syntax to the LTS version of Scala so that code written in this version can already use it. The current LTS would then support old and new-style givens indefinitely, whereas new Scala 3.x versions would phase out the old syntax over time. + 1. A simple typeclass instance, such as `Ord[Int]`. + 2. A parameterized type class instance, such as `Ord` for lists. + 3. A type class instance with an explicit context parameter. + 4. A type class instance with a named eexplicit context parameter. + 4. A simple given alias. + 5. A parameterized given alias + 6. A given alias with an explicit context parameter. + 8. An abstract or deferred given + 9. A by-name given, e.g. if we have a given alias of a mutable variable, and we + want to make sure that it gets re-evaluated on each access. +```scala + // Simple typeclass + given Ord[Int]: + def compare(x: Int, y: Int) = ... + // Parameterized typeclass with context bound + given [A: Ord] => Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ... -### Abolish Abstract Givens + // Parameterized typeclass with context parameter + given [A] => Ord[A] => Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ... -Another simplification is possible. So far we have special syntax for abstract givens: -```scala -given x: T + // Parameterized typeclass with named context parameter + given [A] => (ord: Ord[A]) => Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ... + + // Simple alias + given Ord[Int] = IntOrd() + + // Parameterized alias with context bound + given [A: Ord] => Ord[List[A]] = + ListOrd[A] + + // Parameterized alias with context parameter + given [A] => Ord[A] => Ord[List[A]] = + ListOrd[A] + + // Abstract or deferred given + given Context = deferred + + // By-name given + given () => Context = curCtx ``` -The problem is that this syntax clashes with the quite common case where we want to establish a given without any nested definitions. For instance -consider a given that constructs a type tag: +Here are the same examples, with optional names provided: ```scala -class Tag[T] + // Simple typeclass + given intOrd: Ord[Int]: + def compare(x: Int, y: Int) = ... + + // Parameterized typeclass with context bound + given listOrd: [A: Ord] => Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ... + + // Parameterized typeclass with context parameter + given listOrd: [A] => Ord[A] => Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ... + + // Parameterized typeclass with named context parameter + given listOrd: [A] => (ord: Ord[A]) => Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ... + + // Simple alias + given intOrd: Ord[Int] = IntOrd() + + // Parameterized alias with context bound + given listOrd: [A: Ord] => Ord[List[A]] = + ListOrd[A] + + // Parameterized alias with context parameter + given listOrd: [A] => Ord[A] => Ord[List[A]] = + ListOrd[A] + + // Abstract or deferred given + given context: Context = deferred + + // By-name given + given context: () => Context = curCtx ``` -Then this works: + +**By Name Givens** + +We sometimes find it necessary that a given alias is re-evaluated each time it is called. For instance, say we have a mutable variable `curCtx` and we want to define a given that returns the current value of that variable. A normal given alias will not do since by default given aliases are mapped to +lazy vals. + +In general, we want to avoid re-evaluation of the given. But there are situations like the one above where we want to specify _by-name_ evaluation instead. The proposed new syntax for this is shown in the last clause above. This is arguably the a natural way to express by-name givens. We want to use a conditional given, since these map to methods, but the set of preconditions is empty, hence the `()` parameter. Equivalently, under the context function viewpoint, we are defining a context function of the form `() ?=> T`, and these are equivalent to by-name parameters. + +Compare with the current best way to do achieve this, which is to use a dummy type parameter. ```scala -given Tag[String]() -given Tag[String] with {} + given [DummySoThatItsByName]: Context = curCtx ``` -But the following more natural syntax fails: +This has the same effect, but feels more like a hack than a clean solution. + +**Dropping `with`** + +In the new syntax, all typeclass instances introduce definitions like normal +class bodies, enclosed in braces `{...}` or following a `:`. The irregular +requirement to use `with` is dropped. In retrospect, the main reason to introduce `with` was since a definition like + ```scala -given Tag[String] -``` -The last line gives a rather cryptic error: +given [A](using Ord[A]): Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ... ``` -1 |given Tag[String] - | ^ - | anonymous given cannot be abstract +was deemed to be too cryptic, with the double meaning of colons. But since that syntax is gone, we don't need `with` anymore. There's still a double meaning of colons, e.g. in +```scala +given intOrd: Ord[Int]: + ... ``` -The problem is that the compiler thinks that the last given is intended to be abstract, and complains since abstract givens need to be named. This is another annoying dissonance. Nowhere else in Scala's syntax does adding a -`()` argument to a class cause a drastic change in meaning. And it's also a violation of the principle that it should be possible to define all givens without providing names for them. +but since now both uses of `:` are very familiar (type ascription _vs_ start of nested definitions), it's manageable. Besides, the problem occurs only for named typeclass instances, which should be the exceptional case anyway. -Fortunately, abstract givens are no longer necessary since they are superseded by the new `deferred` scheme. So we can deprecate that syntax over time. Abstract givens are a highly specialized mechanism with a so far non-obvious syntax. We have seen that this syntax clashes with reasonable expectations of Scala programmers. My estimate is that maybe a dozen people world-wide have used abstract givens in anger so far. -**Proposal** In the future, let the `= deferred` mechanism be the only way to deliver the functionality of abstract givens. +**Possible ambiguities** -This is less of a disruption than it might appear at first: +If one wants to define a given for an a actual function type (which is probably not advisable in practice), one needs to enclose the function type in parentheses, i.e. `given ([A] => F[A])`. This is true in the currently implemented syntax and stays true for all discussed change proposals. - - `given T` was illegal before since abstract givens could not be anonymous. - It now means a concrete given of class `T` with no member definitions. - - `given x: T` is legacy syntax for an abstract given. - - `given T as x = deferred` is the analogous new syntax, which is more powerful since - it allows for automatic instantiation. - - `given T = deferred` is the anonymous version in the new syntax, which was not expressible before. +The double meaning of : with optional prefix names is resolved as usual. A : at the end of a line starts a nested definition block. If for some obscure reason one wants to define a named given on multiple lines, one has to format it as follows: +```scala + given intOrd + : Ord = ... +``` -**Benefits:** +**Summary** - - Simplification of the language since a feature is dropped - - Eliminate non-obvious and misleading syntax. +This will be a fairly significant change to the given syntax. I believe there's still a possibility to do this. Not so much code has migrated to new style givens yet, and code that was written can be changed fairly easily. Specifically, there are about a 900K definitions of `implicit def`s +in Scala code on Github and about 10K definitions of `given ... with`. So about 1% of all code uses the Scala 3 syntax, which would have to be changed again. + +Changing something introduced just recently in Scala 3 is not fun, +but I believe these adjustments are preferable to let bad syntax +sit there and fester. The cost of changing should be amortized by improved developer experience over time, and better syntax would also help in migrating Scala 2 style implicits to Scala 3. But we should do it quickly before a lot more code +starts migrating. + +Migration to the new syntax is straightforward, and can be supported by automatic rewrites. For a transition period we can support both the old and the new syntax. It would be a good idea to backport the new given syntax to the LTS version of Scala so that code written in this version can already use it. The current LTS would then support old and new-style givens indefinitely, whereas new Scala 3.x versions would phase out the old syntax over time. ### Bonus: Fixing Singleton @@ -586,7 +700,7 @@ Here are some standard type classes, which were mostly already introduced at the def maximum[T: Ord](xs: List[T]): T = xs.reduce(_ `max` _) - given [T: Ord] => T is Ord as descending: + given descending: [T: Ord] => T is Ord: extension (x: T) def compareTo(y: T) = T.compareTo(y)(x) def minimum[T: Ord](xs: List[T]) = diff --git a/tests/neg/deferred-givens-2.scala b/tests/neg/deferred-givens-2.scala index 4e75ceb08728..9a95271a4f46 100644 --- a/tests/neg/deferred-givens-2.scala +++ b/tests/neg/deferred-givens-2.scala @@ -12,7 +12,7 @@ object Scoped: class SortedIntCorrect2 extends Sorted: type Element = Int - override given (Int is Ord)() as given_Ord_Element + override given given_Ord_Element: (Int is Ord)() class SortedIntWrong1 extends Sorted: // error type Element = Int diff --git a/tests/neg/deferred-givens.scala b/tests/neg/deferred-givens.scala index 7ff67d784714..8a1bcb2b50fc 100644 --- a/tests/neg/deferred-givens.scala +++ b/tests/neg/deferred-givens.scala @@ -5,7 +5,7 @@ class Ctx class Ctx2 trait A: - given Ctx as ctx = deferred + given ctx: Ctx = deferred given Ctx2 = deferred class B extends A // error @@ -13,7 +13,7 @@ class B extends A // error abstract class C extends A // error class D extends A: - given Ctx as ctx = Ctx() // ok, was implemented + given ctx: Ctx = Ctx() // ok, was implemented given Ctx2 = Ctx2() // ok class Ctx3[T] diff --git a/tests/neg/i13580.check b/tests/neg/i13580.check new file mode 100644 index 000000000000..8f91bcf9bde5 --- /dev/null +++ b/tests/neg/i13580.check @@ -0,0 +1,4 @@ +-- Error: tests/neg/i13580.scala:9:7 ----------------------------------------------------------------------------------- +9 |given (using tracked val w: IntWidth) => IntCandidate: // error + | ^^^^^ + | `using` is already implied here, should not be given explicitly diff --git a/tests/neg/i13580.scala b/tests/neg/i13580.scala new file mode 100644 index 000000000000..7388ee532526 --- /dev/null +++ b/tests/neg/i13580.scala @@ -0,0 +1,13 @@ +//> using options -language:experimental.modularity -source future +trait IntWidth: + type Out +given IntWidth: + type Out = 155 + +trait IntCandidate: + type Out +given (using tracked val w: IntWidth) => IntCandidate: // error + type Out = w.Out + +val x = summon[IntCandidate] +val xx = summon[x.Out =:= 155] diff --git a/tests/neg/i8150.scala b/tests/neg/i8150.scala index 2f0505c6265a..b7edceec9426 100644 --- a/tests/neg/i8150.scala +++ b/tests/neg/i8150.scala @@ -1,3 +1,3 @@ trait A trait B -type T = {given(using a: A) as B} // error: refinement cannot be `given` \ No newline at end of file +type T = {given x(using a: A): B} // error: refinement cannot be `given` \ No newline at end of file diff --git a/tests/pos/deferred-givens.scala b/tests/pos/deferred-givens.scala index b9018c97e151..0ad751fcc7e0 100644 --- a/tests/pos/deferred-givens.scala +++ b/tests/pos/deferred-givens.scala @@ -1,7 +1,7 @@ //> using options -language:experimental.modularity -source future import compiletime.* class Ord[Elem] -given Ord[Double] +given Ord[Double]() trait A: type Elem : Ord diff --git a/tests/pos/given-syntax.scala b/tests/pos/given-syntax.scala new file mode 100644 index 000000000000..a8b64c1030a7 --- /dev/null +++ b/tests/pos/given-syntax.scala @@ -0,0 +1,120 @@ +//> using options -language:experimental.modularity -source future + +class Context +class Ord[T] + +class Ordered: + type Self + +class Monoid[T] +import compiletime.deferred + +def IntOrd[A](): Ord[Int] = ??? +def ListOrd[A](using Ord[A]): Ord[List[A]] = ??? +def curCtx: Context = ??? + +trait anon1: + // Simple typeclass + given Ord[Int]: + def compare(x: Int, y: Int) = ??? + + // Simple type class with extension method: + given Monoid[Int]: + extension (x: Int) + def combine(y: Int) = x + y + def unit = 0 + + // Parameterized typeclass with context bound + given [A: Ord] => Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ??? + +trait anon2: + // Parameterized typeclass with context parameter + given [A] => Ord[A] => Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ??? + +trait anon3: + // Parameterized typeclass with named context parameter + given [A] => (ord: Ord[A]) => Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ??? + + // Simple alias + given Ord[Int] = IntOrd() + +trait anon4: + // Parameterized alias with context bound + given [A: Ord] => Ord[List[A]] = + ListOrd[A] + +trait anon5: + // Parameterized alias with context parameter + given [A] => Ord[A] => Ord[List[A]] = + ListOrd[A] + + given [A] => A is Ordered => List[A] is Ordered = + ??? + +trait anon6: + // Parameterized alias with named context parameter + given [A] => (ord: Ord[A]) => Ord[List[A]] = + ListOrd[A](using ord) + + given [A] => (A is Ordered) => List[A] is Ordered = + ??? + + // Concrete class instance + given Context() + +trait anon7: + // Abstract or deferred given + given Context = deferred + +trait anon8: + // By-name given + given () => Context = curCtx + +trait named: + given intOrd: Ord[Int]: + def compare(x: Int, y: Int) = ??? + + // Simple type class with extension method: + given intMonoid: Monoid[Int]: + extension (x: Int) + def combine(y: Int) = x + y + def unit = 0 + + // Parameterized typeclass with context bound + given listOrd: [A: Ord] => Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ??? + + // Parameterized typeclass with context parameter + given listOrd2: [A] => Ord[A] => Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ??? + + // Parameterized typeclass with named context parameter + given listOrd3: [A] => (ord: Ord[A]) => Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ??? + + // Simple alias + given intOrd2: Ord[Int] = IntOrd() + + // Parameterized alias with context bound + given listOrd4: [A: Ord] => Ord[List[A]] = + ListOrd[A] + + // Parameterized alias with context parameter + given listOrd5: [A] => Ord[A] => Ord[List[A]] = + ListOrd[A] + + // Parameterized alias with named context parameter + given listOrd6: [A] => (ord: Ord[A]) => Ord[List[A]] = + ListOrd[A](using ord) + + // Concrete class instance + given context: Context() + + // Abstract or deferred given + given context2: Context = deferred + + // By-name given + given context3: () => Context = curCtx \ No newline at end of file diff --git a/tests/pos/hylolib-cb/HyArray.scala b/tests/pos/hylolib-cb/HyArray.scala index 0fff45e744ec..e4ccab000448 100644 --- a/tests/pos/hylolib-cb/HyArray.scala +++ b/tests/pos/hylolib-cb/HyArray.scala @@ -161,9 +161,9 @@ object HyArray { } -given [T: Value] => Value[HyArray[T]] with { +given [T: Value] => Value[HyArray[T]]: - extension (self: HyArray[T]) { + extension (self: HyArray[T]) def copy(): HyArray[T] = self.copy() @@ -173,17 +173,14 @@ given [T: Value] => Value[HyArray[T]] with { def hashInto(hasher: Hasher): Hasher = self.reduce(hasher, (h, e) => e.hashInto(h)) +end given - } - -} - -given [T: Value] => Collection[HyArray[T]] with { +given [T: Value] => Collection[HyArray[T]]: type Element = T type Position = Int - extension (self: HyArray[T]) { + extension (self: HyArray[T]) // NOTE: Having to explicitly override means that primary declaration can't automatically // specialize trait requirements. @@ -198,10 +195,7 @@ given [T: Value] => Collection[HyArray[T]] with { def positionAfter(p: Int) = p + 1 def at(p: Int) = self.at(p) - - } - -} +end given // NOTE: This should work. // given hyArrayIsStringConvertible[T](using diff --git a/tests/pos/i13580.scala b/tests/pos/i13580.scala index c3c491a19dbe..60e971610209 100644 --- a/tests/pos/i13580.scala +++ b/tests/pos/i13580.scala @@ -6,7 +6,7 @@ given IntWidth: trait IntCandidate: type Out -given (using tracked val w: IntWidth) => IntCandidate: +given (tracked val w: IntWidth) => IntCandidate: type Out = w.Out val x = summon[IntCandidate] diff --git a/tests/pos/typeclasses-arrow.scala b/tests/pos/typeclasses-arrow.scala index 379365ffa1c5..4b2a25122b0d 100644 --- a/tests/pos/typeclasses-arrow.scala +++ b/tests/pos/typeclasses-arrow.scala @@ -36,7 +36,7 @@ end Common object Instances extends Common: - given Int is Ord as intOrd: + given intOrd: Int is Ord: extension (x: Int) def compareTo(y: Int) = if x < y then -1 @@ -52,7 +52,7 @@ object Instances extends Common: val fst = x.compareTo(y) if (fst != 0) fst else xs1.compareTo(ys1) - given List is Monad as listMonad: + given listMonad: List is Monad: extension [A](xs: List[A]) def flatMap[B](f: A => List[B]): List[B] = xs.flatMap(f) def pure[A](x: A): List[A] = @@ -60,7 +60,7 @@ object Instances extends Common: type Reader[Ctx] = [X] =>> Ctx => X - given [Ctx] => Reader[Ctx] is Monad as readerMonad: + given readerMonad: [Ctx] => Reader[Ctx] is Monad: extension [A](r: Ctx => A) def flatMap[B](f: A => Ctx => B): Ctx => B = ctx => f(r(ctx))(ctx) def pure[A](x: A): Ctx => A = @@ -82,7 +82,7 @@ object Instances extends Common: def maximum[T: Ord](xs: List[T]): T = xs.reduce(_ `max` _) - given [T: Ord] => T is Ord as descending: + given descending: [T: Ord] => T is Ord: extension (x: T) def compareTo(y: T) = T.compareTo(y)(x) def minimum[T: Ord](xs: List[T]) = diff --git a/tests/pos/typeclasses-arrow0.scala b/tests/pos/typeclasses-arrow0.scala index 22d84fe6478d..d7d85e6b7400 100644 --- a/tests/pos/typeclasses-arrow0.scala +++ b/tests/pos/typeclasses-arrow0.scala @@ -32,14 +32,14 @@ end Common object Instances extends Common: - given Ord[Int] as intOrd: + given intOrd: Ord[Int]: extension (x: Int) def compareTo(y: Int) = if x < y then -1 else if x > y then +1 else 0 - given [T: Ord] => Ord[List[T]]: + given listOrd: [T: Ord] => Ord[List[T]]: extension (xs: List[T]) def compareTo(ys: List[T]): Int = (xs, ys) match case (Nil, Nil) => 0 case (Nil, _) => -1 @@ -48,7 +48,7 @@ object Instances extends Common: val fst = x.compareTo(y) if (fst != 0) fst else xs1.compareTo(ys1) - given Monad[List] as listMonad: + given listMonad: Monad[List]: extension [A](xs: List[A]) def flatMap[B](f: A => List[B]): List[B] = xs.flatMap(f) def pure[A](x: A): List[A] = @@ -56,7 +56,7 @@ object Instances extends Common: type Reader[Ctx] = [X] =>> Ctx => X - given [Ctx] => Monad[Reader[Ctx]] as readerMonad: + given readerMonad: [Ctx] => Monad[Reader[Ctx]]: extension [A](r: Ctx => A) def flatMap[B](f: A => Ctx => B): Ctx => B = ctx => f(r(ctx))(ctx) def pure[A](x: A): Ctx => A = @@ -78,7 +78,7 @@ object Instances extends Common: def maximum[T: Ord](xs: List[T]): T = xs.reduce(_ `max` _) - given [T: Ord] => Ord[T] as descending: + given descending: [T: Ord] => Ord[T]: extension (x: T) def compareTo(y: T) = summon[Ord[T]].compareTo(y)(x) def minimum[T: Ord](xs: List[T]) = diff --git a/tests/run/Providers.check b/tests/run/Providers.check new file mode 100644 index 000000000000..7b0a9a8b143e --- /dev/null +++ b/tests/run/Providers.check @@ -0,0 +1,20 @@ +11 +hi +List(1, 2, 3) +hi + +Direct: +You've just been subscribed to RockTheJVM. Welcome, Daniel +Acquired connection +Executing query: insert into subscribers(name, email) values Daniel daniel@RocktheJVM.com +You've just been subscribed to RockTheJVM. Welcome, Martin +Acquired connection +Executing query: insert into subscribers(name, email) values Martin odersky@gmail.com + +Injected +You've just been subscribed to RockTheJVM. Welcome, Daniel +Acquired connection +Executing query: insert into subscribers(name, email) values Daniel daniel@RocktheJVM.com +You've just been subscribed to RockTheJVM. Welcome, Martin +Acquired connection +Executing query: insert into subscribers(name, email) values Martin odersky@gmail.com diff --git a/tests/run/Providers.scala b/tests/run/Providers.scala new file mode 100644 index 000000000000..3eb4b2df2207 --- /dev/null +++ b/tests/run/Providers.scala @@ -0,0 +1,177 @@ +import language.experimental.modularity +import compiletime.constValue +import compiletime.ops.int.S + +// Featherweight dependency injection library, inspired by the use case +// laid out in the ZIO course of RockTheJVM. + +/** Some things that are not part of Tuple yet, but that would be nice to have. */ +object TupleUtils: + + /** The index of the first element type of the tuple `Xs` that is a subtype of `X` */ + type IndexOf[Xs <: Tuple, X] <: Int = Xs match + case X *: _ => 0 + case _ *: ys => S[IndexOf[ys, X]] + + /** A trait describing a selection from a tuple `Xs` returning an element of type `X` */ + trait Select[Xs <: Tuple, X]: + def apply(xs: Xs): X + + /** A given implementing `Select` to return the first element of tuple `Xs` + * that has a static type matching `X`. + */ + given [Xs <: NonEmptyTuple, X] => (idx: ValueOf[IndexOf[Xs, X]]) => Select[Xs, X]: + def apply(xs: Xs) = xs.apply(idx.value).asInstanceOf[X] + +/** A featherweight library for dependency injection */ +object Providers: + import TupleUtils.* + + /** A provider is a zero-cost wrapper around a type that is intended + * to be passed implicitly + */ + opaque type Provider[T] = T + + def provide[X](x: X): Provider[X] = x + + def provided[X](using p: Provider[X]): X = p + + /** Project a provider to one of its element types */ + given [Xs <: Tuple, X] => (ps: Provider[Xs], select: Select[Xs, X]) => Provider[X] = + select(ps) + + /** Form a compound provider wrapping a tuple */ + given [X, Xs <: Tuple] => (p: Provider[X], ps: Provider[Xs]) => Provider[X *: Xs] = + p *: ps + + given Provider[EmptyTuple] = EmptyTuple + +end Providers + +@main def Test = + import TupleUtils.* + + type P = (Int, String, List[Int]) + val x: P = (11, "hi", List(1, 2, 3)) + val selectInt = summon[Select[P, Int]] + println(selectInt(x)) + val selectString = summon[Select[P, String]] + println(selectString(x)) + val selectList = summon[Select[P, List[Int]]] + println(selectList(x)) + val selectObject = summon[Select[P, Object]] + println(selectObject(x)) // prints "hi" + println(s"\nDirect:") + Explicit().test() + println(s"\nInjected") + Injected().test() + +/** Demonstrator for explicit dependency construction */ +class Explicit: + + case class User(name: String, email: String) + + class UserSubscription(emailService: EmailService, db: UserDatabase): + def subscribe(user: User) = + emailService.email(user) + db.insert(user) + + class EmailService: + def email(user: User) = + println(s"You've just been subscribed to RockTheJVM. Welcome, ${user.name}") + + class UserDatabase(pool: ConnectionPool): + def insert(user: User) = + val conn = pool.get() + conn.runQuery(s"insert into subscribers(name, email) values ${user.name} ${user.email}") + + class ConnectionPool(n: Int): + def get(): Connection = + println(s"Acquired connection") + Connection() + + class Connection(): + def runQuery(query: String): Unit = + println(s"Executing query: $query") + + def test() = + val subscriptionService = + UserSubscription( + EmailService(), + UserDatabase( + ConnectionPool(10) + ) + ) + + def subscribe(user: User) = + val sub = subscriptionService + sub.subscribe(user) + + subscribe(User("Daniel", "daniel@RocktheJVM.com")) + subscribe(User("Martin", "odersky@gmail.com")) + +end Explicit + +/** The same application as `Explicit` but using dependency injection */ +class Injected: + import Providers.* + + case class User(name: String, email: String) + + class UserSubscription(using Provider[(EmailService, UserDatabase)]): + def subscribe(user: User) = + provided[EmailService].email(user) + provided[UserDatabase].insert(user) + + class EmailService: + def email(user: User) = + println(s"You've just been subscribed to RockTheJVM. Welcome, ${user.name}") + + class UserDatabase(using Provider[ConnectionPool]): + def insert(user: User) = + val conn = provided[ConnectionPool].get() + conn.runQuery(s"insert into subscribers(name, email) values ${user.name} ${user.email}") + + class ConnectionPool(n: Int): + def get(): Connection = + println(s"Acquired connection") + Connection() + + class Connection(): + def runQuery(query: String): Unit = + println(s"Executing query: $query") + + def test() = + given Provider[EmailService] = provide(EmailService()) + given Provider[ConnectionPool] = provide(ConnectionPool(10)) + given Provider[UserDatabase] = provide(UserDatabase()) + given Provider[UserSubscription] = provide(UserSubscription()) + + def subscribe(user: User)(using Provider[UserSubscription]) = + val sub = provided[UserSubscription] + sub.subscribe(user) + + subscribe(User("Daniel", "daniel@RocktheJVM.com")) + subscribe(User("Martin", "odersky@gmail.com")) + end test + + // explicit version, not used here + object explicit: + val subscriptionService = + UserSubscription( + using provide( + EmailService(), + UserDatabase( + using provide( + ConnectionPool(10) + ) + ) + ) + ) + + given Provider[UserSubscription] = provide(subscriptionService) + end explicit +end Injected + + + diff --git a/tests/run/byname-given.scala b/tests/run/byname-given.scala new file mode 100644 index 000000000000..d18ebb221a68 --- /dev/null +++ b/tests/run/byname-given.scala @@ -0,0 +1,9 @@ +//> using options -language:experimental.modularity -source future + +@main def Test = + var x: Int = 0 + given () => Int = x + assert(summon[Int] == 0) + x += 1 + assert(summon[Int] == 1) + diff --git a/tests/warn/abstract-givens-new.check b/tests/warn/abstract-givens-new.check new file mode 100644 index 000000000000..197d9bcb4f3e --- /dev/null +++ b/tests/warn/abstract-givens-new.check @@ -0,0 +1,5 @@ +-- Warning: tests/warn/abstract-givens-new.scala:7:22 ------------------------------------------------------------------ +7 | given intC: Int is C // warn + | ^ + | This defines an abstract given, which is deprecated. Use a `deferred` given instead. + | Or, if you intend to define a concrete given, follow the type with `()` arguments. diff --git a/tests/warn/abstract-givens-new.scala b/tests/warn/abstract-givens-new.scala new file mode 100644 index 000000000000..b38fd11c4458 --- /dev/null +++ b/tests/warn/abstract-givens-new.scala @@ -0,0 +1,9 @@ +//> using options -language:experimental.modularity -source future +class C: + type Self + +trait T: + given Int is C // ok + given intC: Int is C // warn + given intC2: (Int is C)() // ok + given intC3: Int is C {} // also ok