Description
I'm not sure if this is intentional, but it certainly caught me by surprise.
From the example in the documentation (https://dotty.epfl.ch/docs/reference/contextual/derivation.html):
enum Opt[+T] derives Eq {
case Sm(t: T)
case Nn
}
We can summon a Mirror.Product
for both cases of the enum, but Opt.Nn
is not a subtype of Product
.
The example in the docs suggests that if we have a Mirror.ProductOf[T]
then we can safely cast an instance of T
to Product
(in order to access productIterator
etc.). But this is not the case.
Indeed, if we add a line to the test in the example:
assert(eqoi.eqv(Nn, Nn))
then it throws a ClassCastException at runtime.
Minimized example
(taken from the docs, with a couple of tweaks to make it compile)
import scala.deriving._
import scala.compiletime.{erasedValue, summonInline}
trait Eq[T] {
def eqv(x: T, y: T): Boolean
}
object Eq {
given Eq[Int] {
def eqv(x: Int, y: Int) = x == y
}
inline def summonAll[T <: Tuple]: List[Eq[_]] = inline erasedValue[T] match {
case _: Unit => Nil
case _: (t *: ts) => summonInline[Eq[t]] :: summonAll[ts]
}
def check(elem: Eq[_])(x: Any, y: Any): Boolean =
elem.asInstanceOf[Eq[Any]].eqv(x, y)
def iterator[T](p: T) = p.asInstanceOf[Product].productIterator
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)
}
}
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)
}
}
inline given derived[T](using m: Mirror.Of[T]) as Eq[T] = {
val elemInstances = summonAll[m.MirroredElemTypes]
inline m match {
case s: Mirror.SumOf[T] => eqSum(s, elemInstances)
case p: Mirror.ProductOf[T] => eqProduct(p, elemInstances)
}
}
}
object Test extends App {
import Opt._
val eqoi = summon[Eq[Opt[Int]]]
assert(eqoi.eqv(Sm(23), Sm(23)))
assert(eqoi.eqv(Nn, Nn))
}
Output
java.lang.ClassCastException: myapp.Opt$$anon$1 cannot be cast to scala.Product
Suggested fix
Maybe a documentation fix is sufficient here? i.e. fix the example to avoid the incorrect cast, and add a note about when enum
cases do or do not extend Product
.