Skip to content

Mirror.Product is available for enum values that are not subtypes of Product #9011

Closed
@cb372

Description

@cb372

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions