From bd9e0f7484167a07ae13c4e264a4cc2974aca223 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Fri, 5 May 2023 11:33:10 +0200 Subject: [PATCH 1/6] [docs] Typeclass Derivation no longer recommends `given derived[T]: TC[T]` (#17414) fixes #16916 --- .../reference/contextual/derivation-macro.md | 281 +++++++++--------- docs/_docs/reference/contextual/derivation.md | 148 +++++---- 2 files changed, 227 insertions(+), 202 deletions(-) diff --git a/docs/_docs/reference/contextual/derivation-macro.md b/docs/_docs/reference/contextual/derivation-macro.md index be7565616913..6be3d10df54a 100644 --- a/docs/_docs/reference/contextual/derivation-macro.md +++ b/docs/_docs/reference/contextual/derivation-macro.md @@ -4,15 +4,11 @@ title: "How to write a type class `derived` method using macros" nightlyOf: https://docs.scala-lang.org/scala3/reference/contextual/derivation-macro.html --- -In the main [derivation](./derivation.md) documentation page, we explained the -details behind `Mirror`s and type class derivation. Here we demonstrate how to -implement a type class `derived` method using macros only. We follow the same -example of deriving `Eq` instances and for simplicity we support a `Product` -type e.g., a case class `Person`. The low-level method we will use to implement -the `derived` method exploits quotes, splices of both expressions and types and -the `scala.quoted.Expr.summon` method which is the equivalent of -`summonFrom`. The former is suitable for use in a quote context, used within -macros. +In the main [derivation](./derivation.md) documentation page, we explained the details behind `Mirror`s and type class derivation. +Here we demonstrate how to implement a type class `derived` method using macros only. +We follow the same example of deriving `Eq` instances and for simplicity we support a `Product` type e.g., a case class `Person`. +The low-level technique that we will use to implement the `derived` method exploits quotes, splices of both expressions and types and the `scala.quoted.Expr.summon` method which is the equivalent of `scala.compiletime.summonFrom`. +The former is suitable for use in a quote context, used within macros. As in the original code, the type class definition is the same: @@ -21,185 +17,188 @@ trait Eq[T]: def eqv(x: T, y: T): Boolean ``` -we need to implement a method `Eq.derived` on the companion object of `Eq` that -produces a quoted instance for `Eq[T]`. Here is a possible signature, +We need to implement an inline method `Eq.derived` on the companion object of `Eq` that calls into a macro to produce a quoted instance for `Eq[T]`. +Here is a possible signature: + ```scala -given derived[T: Type](using Quotes): Expr[Eq[T]] +inline def derived[T]: Eq[T] = ${ derivedMacro[T] } + +def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] = ??? ``` -and for comparison reasons we give the same signature we had with `inline`: +Note, that since a type is used in a subsequent macro compilation stage it will need to be lifted to a `quoted.Type` by using the corresponding context bound (seen in `derivedMacro`). + +For comparison, here is the signature of the inline `derived` method from the [main derivation page](./derivation.md): ```scala -inline given derived[T](using Mirror.Of[T]): Eq[T] = ??? +inline def derived[T](using m: Mirror.Of[T]): Eq[T] = ??? ``` -Note, that since a type is used in a subsequent stage it will need to be lifted -to a `Type` by using the corresponding context bound. Also, note that we can -summon the quoted `Mirror` inside the body of the `derived` thus we can omit it -from the signature. The body of the `derived` method is shown below: +Note that the macro-based `derived` signature does not have a `Mirror` parameter. +This is because we can summon the `Mirror` inside the body of `derivedMacro` thus we can omit it from the signature. + +One additional possibility with the body of `derivedMacro` here as opposed to the one with `inline` is that with macros it is simpler to create a fully optimised method body for `eqv`. +Let's say we wanted to derive an `Eq` instance for the following case class `Person`, +```scala +case class Person(name: String, age: Int) derives Eq +``` + +the equality check we are going to generate is the following: ```scala -given derived[T: Type](using Quotes): Expr[Eq[T]] = - import quotes.reflect.* +(x: Person, y: Person) => + summon[Eq[String]].eqv(x.productElement(0), y.productElement(0)) + && summon[Eq[Int]].eqv(x.productElement(1), y.productElement(1)) +``` + +> Note that it is possible, by using the [reflection API](../metaprogramming/reflection.md), to further optimise and directly reference the fields of `Person`, but for clear understanding we will only use quoted expressions. + +The code to generates this body can be seen in the `eqProductBody` method, shown here as part of the definition for the `derivedMacro` method: + + +```scala +def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] = val ev: Expr[Mirror.Of[T]] = Expr.summon[Mirror.Of[T]].get ev match case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} => - val elemInstances = summonAll[elementTypes] + val elemInstances = summonInstances[T, elementTypes] def eqProductBody(x: Expr[Product], y: Expr[Product])(using Quotes): Expr[Boolean] = { - elemInstances.zipWithIndex.foldLeft(Expr(true)) { - case (acc, ('{ $elem: Eq[t] }, index)) => - val indexExpr = Expr(index) - val e1 = '{ $x.productElement($indexExpr).asInstanceOf[t] } - val e2 = '{ $y.productElement($indexExpr).asInstanceOf[t] } - '{ $acc && $elem.eqv($e1, $e2) } - } + if elemInstances.isEmpty then + Expr(true) + else + elemInstances.zipWithIndex.map { + case ('{ $elem: Eq[t] }, index) => + val indexExpr = Expr(index) + val e1 = '{ $x.productElement($indexExpr).asInstanceOf[t] } + val e2 = '{ $y.productElement($indexExpr).asInstanceOf[t] } + '{ $elem.eqv($e1, $e2) } + }.reduce((acc, elem) => '{ $acc && $elem }) + end if } '{ eqProduct((x: T, y: T) => ${eqProductBody('x.asExprOf[Product], 'y.asExprOf[Product])}) } - // case for Mirror.ProductOf[T] - // ... + // case for Mirror.SumOf[T] ... ``` -Note, that in the `inline` case we can merely write -`summonAll[m.MirroredElemTypes]` inside the inline method but here, since -`Expr.summon` is required, we can extract the element types in a macro fashion. -Being inside a macro, our first reaction would be to write the code below. Since -the path inside the type argument is not stable this cannot be used: +Note, that in the version without macros, we can merely write `summonInstances[T, m.MirroredElemTypes]` inside the inline method but here, since `Expr.summon` is required, we can extract the element types in a macro fashion. +Being inside a macro, our first reaction would be to write the code below: ```scala '{ - summonAll[$m.MirroredElemTypes] + summonInstances[T, $m.MirroredElemTypes] } ``` -Instead we extract the tuple-type for element types using pattern matching over -quotes and more specifically of the refined type: +However, since the path inside the type argument is not stable this cannot be used. +Instead we extract the tuple-type for element types using pattern matching over quotes and more specifically of the refined type: ```scala case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} => ... ``` -Shown below is the implementation of `summonAll` as a macro. We assume that -given instances for our primitive types exist. - -```scala -def summonAll[T: Type](using Quotes): List[Expr[Eq[_]]] = - Type.of[T] match - case '[String *: tpes] => '{ summon[Eq[String]] } :: summonAll[tpes] - case '[Int *: tpes] => '{ summon[Eq[Int]] } :: summonAll[tpes] - case '[tpe *: tpes] => derived[tpe] :: summonAll[tpes] - case '[EmptyTuple] => Nil -``` - -One additional difference with the body of `derived` here as opposed to the one -with `inline` is that with macros we need to synthesize the body of the code during the -macro-expansion time. That is the rationale behind the `eqProductBody` function. -Assuming that we calculate the equality of two `Person`s defined with a case -class that holds a name of type [`String`](https://scala-lang.org/api/3.x/scala/Predef$.html#String-0) -and an age of type `Int`, the equality check we want to generate is the following: - -```scala - true - && Eq[String].eqv(x.productElement(0),y.productElement(0)) - && Eq[Int].eqv(x.productElement(1), y.productElement(1)) -``` - -## Calling the derived method inside the macro +Shown below is the implementation of `summonInstances` as a macro, which for each type `elem` in the tuple type, calls +`deriveOrSummon[T, elem]`. -Following the rules in [Macros](../metaprogramming/metaprogramming.md) we create two methods. -One that hosts the top-level splice `eqv` and one that is the implementation. -Alternatively and what is shown below is that we can call the `eqv` method -directly. The `eqGen` can trigger the derivation. +To understand `deriveOrSummon`, consider that if `elem` derives from the parent `T` type, then it is a recursive derivation. +Recursive derivation usually happens for types such as `scala.collection.immutable.::`. If `elem` does not derive from `T`, then there must exist a contextual `Eq[elem]` instance. ```scala -extension [T](inline x: T) - inline def === (inline y: T)(using eq: Eq[T]): Boolean = eq.eqv(x, y) - -inline given eqGen[T]: Eq[T] = ${ Eq.derived[T] } -``` - -Note, that we use inline method syntax and we can compare instance such as -`Sm(Person("Test", 23)) === Sm(Person("Test", 24))` for e.g., the following two -types: - -```scala -case class Person(name: String, age: Int) - -enum Opt[+T]: - case Sm(t: T) - case Nn +def summonInstances[T: Type, Elems: Type](using Quotes): List[Expr[Eq[?]]] = + Type.of[Elems] match + case '[elem *: elems] => deriveOrSummon[T, elem] :: summonInstances[T, elems] + case '[EmptyTuple] => Nil + +def deriveOrSummon[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] = + Type.of[Elem] match + case '[T] => deriveRec[T, Elem] + case _ => '{ summonInline[Eq[Elem]] } + +def deriveRec[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] = + Type.of[T] match + case '[Elem] => '{ error("infinite recursive derivation") } + case _ => derivedMacro[Elem] // recursive derivation ``` The full code is shown below: ```scala +import compiletime.* import scala.deriving.* import scala.quoted.* trait Eq[T]: - def eqv(x: T, y: T): Boolean + def eqv(x: T, y: T): Boolean object Eq: - given Eq[String] with - def eqv(x: String, y: String) = x == y - - given Eq[Int] with - def eqv(x: Int, y: Int) = x == y - - def eqProduct[T](body: (T, T) => Boolean): Eq[T] = - new Eq[T]: - def eqv(x: T, y: T): Boolean = body(x, y) - - def eqSum[T](body: (T, T) => Boolean): Eq[T] = - new Eq[T]: - def eqv(x: T, y: T): Boolean = body(x, y) - - def summonAll[T: Type](using Quotes): List[Expr[Eq[_]]] = - Type.of[T] match - case '[String *: tpes] => '{ summon[Eq[String]] } :: summonAll[tpes] - case '[Int *: tpes] => '{ summon[Eq[Int]] } :: summonAll[tpes] - case '[tpe *: tpes] => derived[tpe] :: summonAll[tpes] - case '[EmptyTuple] => Nil - - given derived[T: Type](using q: Quotes): Expr[Eq[T]] = - import quotes.reflect.* - - val ev: Expr[Mirror.Of[T]] = Expr.summon[Mirror.Of[T]].get - - ev match - case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} => - val elemInstances = summonAll[elementTypes] - val eqProductBody: (Expr[T], Expr[T]) => Expr[Boolean] = (x, y) => - elemInstances.zipWithIndex.foldLeft(Expr(true: Boolean)) { - case (acc, (elem, index)) => - val e1 = '{$x.asInstanceOf[Product].productElement(${Expr(index)})} - val e2 = '{$y.asInstanceOf[Product].productElement(${Expr(index)})} - - '{ $acc && $elem.asInstanceOf[Eq[Any]].eqv($e1, $e2) } - } - '{ eqProduct((x: T, y: T) => ${eqProductBody('x, 'y)}) } - - case '{ $m: Mirror.SumOf[T] { type MirroredElemTypes = elementTypes }} => - val elemInstances = summonAll[elementTypes] - val eqSumBody: (Expr[T], Expr[T]) => Expr[Boolean] = (x, y) => - val ordx = '{ $m.ordinal($x) } - val ordy = '{ $m.ordinal($y) } - - val elements = Expr.ofList(elemInstances) - '{ $ordx == $ordy && $elements($ordx).asInstanceOf[Eq[Any]].eqv($x, $y) } - - '{ eqSum((x: T, y: T) => ${eqSumBody('x, 'y)}) } - end derived + given Eq[String] with + def eqv(x: String, y: String) = x == y + + given Eq[Int] with + def eqv(x: Int, y: Int) = x == y + + def eqProduct[T](body: (T, T) => Boolean): Eq[T] = + new Eq[T]: + def eqv(x: T, y: T): Boolean = body(x, y) + + def eqSum[T](body: (T, T) => Boolean): Eq[T] = + new Eq[T]: + def eqv(x: T, y: T): Boolean = body(x, y) + + def summonInstances[T: Type, Elems: Type](using Quotes): List[Expr[Eq[?]]] = + Type.of[Elems] match + case '[elem *: elems] => deriveOrSummon[T, elem] :: summonInstances[T, elems] + case '[EmptyTuple] => Nil + + def deriveOrSummon[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] = + Type.of[Elem] match + case '[T] => deriveRec[T, Elem] + case _ => '{ summonInline[Eq[Elem]] } + + def deriveRec[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] = + import quotes.reflect.* + Type.of[T] match + case '[Elem] => report.errorAndAbort("infinite recursive derivation") + case _ => derivedMacro[Elem] // recursive derivation + + inline def derived[T]: Eq[T] = ${ derivedMacro[T] } + + def derivedMacro[T: Type](using Quotes): Expr[Eq[T]] = + + val ev: Expr[Mirror.Of[T]] = Expr.summon[Mirror.Of[T]].get + + ev match + case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} => + val elemInstances = summonInstances[T, elementTypes] + def eqProductBody(x: Expr[Product], y: Expr[Product])(using Quotes): Expr[Boolean] = { + if elemInstances.isEmpty then + Expr(true) + else + elemInstances.zipWithIndex.map { + case ('{ $elem: Eq[t] }, index) => + val indexExpr = Expr(index) + val e1 = '{ $x.productElement($indexExpr).asInstanceOf[t] } + val e2 = '{ $y.productElement($indexExpr).asInstanceOf[t] } + '{ $elem.eqv($e1, $e2) } + }.reduce((acc, elem) => '{ $acc && $elem }) + end if + } + '{ eqProduct((x: T, y: T) => ${eqProductBody('x.asExprOf[Product], 'y.asExprOf[Product])}) } + + case '{ $m: Mirror.SumOf[T] { type MirroredElemTypes = elementTypes }} => + val elemInstances = summonInstances[T, elementTypes] + val elements = Expr.ofList(elemInstances) + + def eqSumBody(x: Expr[T], y: Expr[T])(using Quotes): Expr[Boolean] = + val ordx = '{ $m.ordinal($x) } + val ordy = '{ $m.ordinal($y) } + '{ $ordx == $ordy && $elements($ordx).asInstanceOf[Eq[Any]].eqv($x, $y) } + + '{ eqSum((x: T, y: T) => ${eqSumBody('x, 'y)}) } + end derivedMacro end Eq - -object Macro3: - extension [T](inline x: T) - inline def === (inline y: T)(using eq: Eq[T]): Boolean = eq.eqv(x, y) - - inline given eqGen[T]: Eq[T] = ${ Eq.derived[T] } ``` diff --git a/docs/_docs/reference/contextual/derivation.md b/docs/_docs/reference/contextual/derivation.md index 5836c58135dc..a4da7c470e3c 100644 --- a/docs/_docs/reference/contextual/derivation.md +++ b/docs/_docs/reference/contextual/derivation.md @@ -309,9 +309,8 @@ worked out example of such a library, see [Shapeless 3](https://github.com/miles ## How to write a type class `derived` method using low level mechanisms -The low-level method we will use to implement a type class `derived` method in this example exploits three new -type-level constructs in Scala 3: inline methods, inline matches, and implicit searches via `summonInline` or `summonFrom`. Given this definition of the -`Eq` type class, +The low-level method we will use to implement a type class `derived` method in this example exploits three new type-level constructs in Scala 3: inline methods, inline matches, and implicit searches via `summonInline` or `summonFrom`. +Given this definition of the `Eq` type class, ```scala trait Eq[T]: @@ -319,77 +318,98 @@ trait Eq[T]: ``` we need to implement a method `Eq.derived` on the companion object of `Eq` that produces a given instance for `Eq[T]` given -a `Mirror[T]`. Here is a possible implementation, +a `Mirror[T]`. +Here is a possible implementation, ```scala import scala.deriving.Mirror -inline given derived[T](using m: Mirror.Of[T]): Eq[T] = - val elemInstances = summonAll[m.MirroredElemTypes] // (1) - inline m match // (2) +inline def derived[T](using m: Mirror.Of[T]): Eq[T] = + lazy val elemInstances = summonInstances[T, m.MirroredElemTypes] // (1) + inline m match // (2) case s: Mirror.SumOf[T] => eqSum(s, elemInstances) case p: Mirror.ProductOf[T] => eqProduct(p, elemInstances) ``` -Note that `derived` is defined as an `inline` given. This means that the method will be expanded at -call sites (for instance the compiler generated instance definitions in the companion objects of ADTs which have a -`derived Eq` clause), and also that it can be used recursively if necessary, to compute instances for children. +Note that `derived` is defined as an `inline def`. +This means that the method will be inlined at all call sites (for instance the compiler generated instance definitions in the companion objects of ADTs which have a `deriving Eq` clause). -The body of this method (1) first materializes the `Eq` instances for all the child types of type the instance is -being derived for. This is either all the branches of a sum type or all the fields of a product type. The -implementation of `summonAll` is `inline` and uses Scala 3's `summonInline` construct to collect the instances as a -`List`, +> Inlining of complex code is potentially expensive if overused (meaning slower compile times) so we should be careful to limit how many times `derived` is called for the same type. +> For example, when computing an instance for a sum type, it may be necessary to call `derived` recursively to compute an instance for a one of its child cases. +> That child case may in turn be a product type, that declares a field referring back to the parent sum type. +> To compute the instance for this field, we should not call `derived` recursively, but instead summon from the context. +> Typically the found given instance will be the root given instance that initially called `derived`. + +The body of `derived` (1) first materializes the `Eq` instances for all the child types of type the instance is being derived for. +This is either all the branches of a sum type or all the fields of a product type. +The implementation of `summonInstances` is `inline` and uses Scala 3's `summonInline` construct to collect the instances as a `List`, ```scala -inline def summonAll[T <: Tuple]: List[Eq[_]] = - inline erasedValue[T] match +inline def summonInstances[T, Elems <: Tuple]: List[Eq[?]] = + inline erasedValue[Elems] match + case _: (elem *: elems) => deriveOrSummon[T, elem] :: summonInstances[T, elems] case _: EmptyTuple => Nil - case _: (t *: ts) => summonInline[Eq[t]] :: summonAll[ts] + +inline def deriveOrSummon[T, Elem]: Eq[Elem] = + inline erasedValue[Elem] match + case _: T => deriveRec[T, Elem] + case _ => summonInline[Eq[Elem]] + +inline def deriveRec[T, Elem]: Eq[Elem] = + inline erasedValue[T] match + case _: Elem => error("infinite recursive derivation") + case _ => Eq.derived[Elem](using summonInline[Mirror.Of[Elem]]) // recursive derivation ``` with the instances for children in hand the `derived` method uses an `inline match` to dispatch to methods which can -construct instances for either sums or products (2). Note that because `derived` is `inline` the match will be -resolved at compile-time and only the left-hand side of the matching case will be inlined into the generated code with -types refined as revealed by the match. +construct instances for either sums or products (2). +Note that because `derived` is `inline` the match will be resolved at compile-time and only the right-hand side of the matching case will be inlined into the generated code with types refined as revealed by the match. -In the sum case, `eqSum`, we use the runtime `ordinal` values of the arguments to `eqv` to first check if the two -values are of the same subtype of the ADT (3) and then, if they are, to further test for equality based on the `Eq` -instance for the appropriate ADT subtype using the auxiliary method `check` (4). +In the sum case, `eqSum`, we use the runtime `ordinal` values of the arguments to `eqv` to first check if the two values are of the same subtype of the ADT (3) and then, if they are, to further test for equality based on the `Eq` instance for the appropriate ADT subtype using the auxiliary method `check` (4). ```scala import scala.deriving.Mirror -def eqSum[T](s: Mirror.SumOf[T], elems: List[Eq[_]]): Eq[T] = +def eqSum[T](s: Mirror.SumOf[T], elems: => List[Eq[?]]): Eq[T] = new Eq[T]: def eqv(x: T, y: T): Boolean = val ordx = s.ordinal(x) // (3) - (s.ordinal(y) == ordx) && check(elems(ordx))(x, y) // (4) + (s.ordinal(y) == ordx) && check(x, y, elems(ordx)) // (4) ``` -In the product case, `eqProduct` we test the runtime values of the arguments to `eqv` for equality as products based -on the `Eq` instances for the fields of the data type (5), +In the product case, `eqProduct` we test the runtime values of the arguments to `eqv` for equality as products based on the `Eq` instances for the fields of the data type (5), ```scala import scala.deriving.Mirror -def eqProduct[T](p: Mirror.ProductOf[T], elems: List[Eq[_]]): Eq[T] = +def eqProduct[T](p: Mirror.ProductOf[T], elems: => List[Eq[?]]): Eq[T] = new Eq[T]: def eqv(x: T, y: T): Boolean = - iterator(x).zip(iterator(y)).zip(elems.iterator).forall { // (5) - case ((x, y), elem) => check(elem)(x, y) - } + iterable(x).lazyZip(iterable(y)).lazyZip(elems).forall(check) ``` +Both `eqSum` and `eqProduct` have a by-name parameter `elems`, because the argument passed is the reference to the lazy `elemInstances` value. + Pulling this all together we have the following complete implementation, ```scala import scala.deriving.* -import scala.compiletime.{erasedValue, summonInline} +import scala.compiletime.{error, erasedValue, summonInline} -inline def summonAll[T <: Tuple]: List[Eq[_]] = - inline erasedValue[T] match +inline def summonInstances[T, Elems <: Tuple]: List[Eq[?]] = + inline erasedValue[Elems] match + case _: (elem *: elems) => deriveOrSummon[T, elem] :: summonInstances[T, elems] case _: EmptyTuple => Nil - case _: (t *: ts) => summonInline[Eq[t]] :: summonAll[ts] + +inline def deriveOrSummon[T, Elem]: Eq[Elem] = + inline erasedValue[Elem] match + case _: T => deriveRec[T, Elem] + case _ => summonInline[Eq[Elem]] + +inline def deriveRec[T, Elem]: Eq[Elem] = + inline erasedValue[T] match + case _: Elem => error("infinite recursive derivation") + case _ => Eq.derived[Elem](using summonInline[Mirror.Of[Elem]]) // recursive derivation trait Eq[T]: def eqv(x: T, y: T): Boolean @@ -398,26 +418,25 @@ object Eq: given Eq[Int] with def eqv(x: Int, y: Int) = x == y - def check(elem: Eq[_])(x: Any, y: Any): Boolean = + def check(x: Any, y: Any, elem: Eq[?]): Boolean = elem.asInstanceOf[Eq[Any]].eqv(x, y) - def iterator[T](p: T) = p.asInstanceOf[Product].productIterator + def iterable[T](p: T): Iterable[Any] = new AbstractIterable[Any]: + def iterator: Iterator[Any] = p.asInstanceOf[Product].productIterator - def eqSum[T](s: Mirror.SumOf[T], elems: => List[Eq[_]]): Eq[T] = + def eqSum[T](s: Mirror.SumOf[T], elems: => List[Eq[?]]): Eq[T] = new Eq[T]: def eqv(x: T, y: T): Boolean = val ordx = s.ordinal(x) - (s.ordinal(y) == ordx) && check(elems(ordx))(x, y) + (s.ordinal(y) == ordx) && check(x, y, elems(ordx)) - def eqProduct[T](p: Mirror.ProductOf[T], elems: => List[Eq[_]]): Eq[T] = + def eqProduct[T](p: Mirror.ProductOf[T], elems: => List[Eq[?]]): Eq[T] = new Eq[T]: def eqv(x: T, y: T): Boolean = - iterator(x).zip(iterator(y)).zip(elems.iterator).forall { - case ((x, y), elem) => check(elem)(x, y) - } + iterable(x).lazyZip(iterable(y)).lazyZip(elems).forall(check) - inline given derived[T](using m: Mirror.Of[T]): Eq[T] = - lazy val elemInstances = summonAll[m.MirroredElemTypes] + inline def derived[T](using m: Mirror.Of[T]): Eq[T] = + lazy val elemInstances = summonInstances[T, m.MirroredElemTypes] inline m match case s: Mirror.SumOf[T] => eqSum(s, elemInstances) case p: Mirror.ProductOf[T] => eqProduct(p, elemInstances) @@ -427,32 +446,39 @@ end Eq we can test this relative to a simple ADT like so, ```scala -enum Opt[+T] derives Eq: - case Sm(t: T) - case Nn +enum Lst[+T] derives Eq: + case Cns(t: T, ts: Lst[T]) + case Nl + +extension [T](t: T) def ::(ts: Lst[T]): Lst[T] = Lst.Cns(t, ts) @main def test(): Unit = - import Opt.* - val eqoi = summon[Eq[Opt[Int]]] - assert(eqoi.eqv(Sm(23), Sm(23))) - assert(!eqoi.eqv(Sm(23), Sm(13))) - assert(!eqoi.eqv(Sm(23), Nn)) + import Lst.* + val eqoi = summon[Eq[Lst[Int]]] + assert(eqoi.eqv(23 :: 47 :: Nl, 23 :: 47 :: Nl)) + assert(!eqoi.eqv(23 :: Nl, 7 :: Nl)) + assert(!eqoi.eqv(23 :: Nl, Nl)) ``` -In this case the code that is generated by the inline expansion for the derived `Eq` instance for `Opt` looks like the +In this case the code that is generated by the inline expansion for the derived `Eq` instance for `Lst` looks like the following, after a little polishing, ```scala -given derived$Eq[T](using eqT: Eq[T]): Eq[Opt[T]] = - eqSum( - summon[Mirror[Opt[T]]], +given derived$Eq[T](using eqT: Eq[T]): Eq[Lst[T]] = + eqSum(summon[Mirror.Of[Lst[T]]], {/* cached lazily */ List( - eqProduct(summon[Mirror[Sm[T]]], List(summon[Eq[T]])), - eqProduct(summon[Mirror[Nn.type]], Nil) + eqProduct(summon[Mirror.Of[Cns[T]]], {/* cached lazily */ + List(summon[Eq[T]], summon[Eq[Lst[T]]]) + }), + eqProduct(summon[Mirror.Of[Nl.type]], {/* cached lazily */ + Nil + }) ) - ) + }) ``` +The `lazy` modifier on `elemInstances` is necessary for preventing infinite recursion in the derived instance for recursive types such as `Lst`. + Alternative approaches can be taken to the way that `derived` methods can be defined. For example, more aggressively inlined variants using Scala 3 macros, whilst being more involved for type class authors to write than the example above, can produce code for type classes like `Eq` which eliminate all the abstraction artefacts (eg. the `Lists` of @@ -466,7 +492,7 @@ given eqSum[A](using inst: => K0.CoproductInstances[Eq, A]): Eq[A] with [t] => (eqt: Eq[t], t0: t, t1: t) => eqt.eqv(t0, t1) ) -given eqProduct[A](using inst: K0.ProductInstances[Eq, A]): Eq[A] with +given eqProduct[A](using inst: => K0.ProductInstances[Eq, A]): Eq[A] with def eqv(x: A, y: A): Boolean = inst.foldLeft2(x, y)(true: Boolean)( [t] => (acc: Boolean, eqt: Eq[t], t0: t, t1: t) => Complete(!eqt.eqv(t0, t1))(false)(true) From 5e7bfb230bd68fd3d46e8af19fc1f78ef25e7d71 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Fri, 5 May 2023 11:42:37 +0200 Subject: [PATCH 2/6] avoid reflect API in error (#17420) small change I forgot to copy over --- docs/_docs/reference/contextual/derivation-macro.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/_docs/reference/contextual/derivation-macro.md b/docs/_docs/reference/contextual/derivation-macro.md index 6be3d10df54a..4b8dcffec846 100644 --- a/docs/_docs/reference/contextual/derivation-macro.md +++ b/docs/_docs/reference/contextual/derivation-macro.md @@ -160,9 +160,8 @@ object Eq: case _ => '{ summonInline[Eq[Elem]] } def deriveRec[T: Type, Elem: Type](using Quotes): Expr[Eq[Elem]] = - import quotes.reflect.* Type.of[T] match - case '[Elem] => report.errorAndAbort("infinite recursive derivation") + case '[Elem] => '{ error("infinite recursive derivation") } case _ => derivedMacro[Elem] // recursive derivation inline def derived[T]: Eq[T] = ${ derivedMacro[T] } From d289bd577ad1d80888751e509d559c08acbcaf07 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 3 Apr 2023 15:34:59 +0200 Subject: [PATCH 3/6] Add scrollbar to the content and sidebar --- .../resources/dotty_res/styles/theme/layout/container.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scaladoc/resources/dotty_res/styles/theme/layout/container.css b/scaladoc/resources/dotty_res/styles/theme/layout/container.css index d71c75e8bda0..a5210210c420 100644 --- a/scaladoc/resources/dotty_res/styles/theme/layout/container.css +++ b/scaladoc/resources/dotty_res/styles/theme/layout/container.css @@ -20,6 +20,10 @@ p { } ::-webkit-scrollbar { - width: 0; + width: 5; background: transparent; } + +::-webkit-scrollbar-thumb { + background: black; +} From cecd5c0c2cd6f4a2a9205979ea194c0e16625b54 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 4 Apr 2023 10:32:37 +0200 Subject: [PATCH 4/6] Add scrollbar in the sidebar --- .../dotty_res/styles/theme/layout/container.css | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/scaladoc/resources/dotty_res/styles/theme/layout/container.css b/scaladoc/resources/dotty_res/styles/theme/layout/container.css index a5210210c420..849235e2fa82 100644 --- a/scaladoc/resources/dotty_res/styles/theme/layout/container.css +++ b/scaladoc/resources/dotty_res/styles/theme/layout/container.css @@ -19,11 +19,18 @@ p { --header-height: calc(8 * var(--base-spacing)); } +/* Scrollbar */ + ::-webkit-scrollbar { - width: 5; + width: 0; background: transparent; } -::-webkit-scrollbar-thumb { - background: black; +#leftColumn ::-webkit-scrollbar{ + width: 5px; } + +#leftColumn ::-webkit-scrollbar-thumb { + background: var(--code-syntax-highlighting-scrollbar); + border-radius: 2px; +} \ No newline at end of file From 86a3becc2e9fc74234406147e4a6a9721ebb91ac Mon Sep 17 00:00:00 2001 From: Anatolii Kmetiuk Date: Fri, 17 Mar 2023 14:59:39 +0100 Subject: [PATCH 5/6] Check the status of coursier download in CoursierScalaTests.scala This should provide more insight on why #17119 happens. --- .../tools/coursier/CoursierScalaTests.scala | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala b/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala index 979fea0684b2..944bf1957d43 100644 --- a/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala +++ b/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala @@ -148,11 +148,11 @@ class CoursierScalaTests: object CoursierScalaTests: - def execCmd(command: String, options: String*): List[String] = + def execCmd(command: String, options: String*): (Int, List[String]) = val cmd = (command :: options.toList).toSeq.mkString(" ") val out = new ListBuffer[String] - cmd.!(ProcessLogger(out += _, out += _)) - out.toList + val code = cmd.!(ProcessLogger(out += _, out += _)) + (code, out.toList) def csScalaCmd(options: String*): List[String] = csCmd("dotty.tools.MainGenericRunner", options*) @@ -166,10 +166,16 @@ object CoursierScalaTests: case Nil => args case _ => "--" +: args val newJOpts = jOpts.map(s => s"--java-opt ${s.stripPrefix("-J")}").mkString(" ") - execCmd("./cs", (s"""launch "org.scala-lang:scala3-compiler_3:${sys.env("DOTTY_BOOTSTRAPPED_VERSION")}" $newJOpts --main-class "$entry" --property "scala.usejavacp=true"""" +: newOptions)*) + execCmd("./cs", (s"""launch "org.scala-lang:scala3-compiler_3:${sys.env("DOTTY_BOOTSTRAPPED_VERSION")}" $newJOpts --main-class "$entry" --property "scala.usejavacp=true"""" +: newOptions)*)._2 /** Get coursier script */ @BeforeClass def setup(): Unit = - val ver = execCmd("uname").head.replace('L', 'l').replace('D', 'd') - execCmd("curl", s"-fLo cs https://git.io/coursier-cli-$ver") #&& execCmd("chmod", "+x cs") + val ver = execCmd("uname")._2.head.replace('L', 'l').replace('D', 'd') + def runAndCheckCmd(cmd: String, options: String*): Unit = + val (code, out) = execCmd(cmd, options*) + if code != 0 then + fail(s"Failed to run $cmd ${options.mkString(" ")}, exit code: $code, output: ${out.mkString("\n")}") + + runAndCheckCmd("curl", s"-fLo cs https://git.io/coursier-cli-$ver") + runAndCheckCmd("chmod", "+x cs") From 3b5b3c04fdf6bc1406c312ad29958ecd3717b102 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Thu, 11 May 2023 09:50:08 +0200 Subject: [PATCH 6/6] revert extra-line --- scaladoc/resources/dotty_res/styles/theme/layout/container.css | 1 - 1 file changed, 1 deletion(-) diff --git a/scaladoc/resources/dotty_res/styles/theme/layout/container.css b/scaladoc/resources/dotty_res/styles/theme/layout/container.css index 1d42c5529f00..53ede0e3dfff 100644 --- a/scaladoc/resources/dotty_res/styles/theme/layout/container.css +++ b/scaladoc/resources/dotty_res/styles/theme/layout/container.css @@ -24,7 +24,6 @@ p { height: auto; } - /* Scrollbar */ ::-webkit-scrollbar {