Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
cb372 opened this issue May 20, 2020 · 4 comments
Closed

Comments

@cb372
Copy link
Contributor

cb372 commented May 20, 2020

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.

@milessabin
Copy link
Contributor

I think Opt.Nn.type should be a subtype of Product. That's certainly the case for case objects.

smarter added a commit that referenced this issue May 22, 2020
Fix #9011: Make single enum values inherit from Product
@smarter
Copy link
Member

smarter commented May 22, 2020

I think Opt.Nn.type should be a subtype of Product. That's certainly the case for case objects.

This is now fixed, but I'm leaving this issue open because the original issue remains I think:

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.

@milessabin Is this cast supposed to be safe? If not, the doc example needs to be fixed.

@smarter smarter reopened this May 22, 2020
@milessabin
Copy link
Contributor

Is this cast supposed to be safe?

Yes it is.

@bishabosha
Copy link
Member

bishabosha commented Oct 16, 2020

I believe that this should be ok to close - I don't think ProductOf will be synthesised unless the underlying type extends Product (invariant, not checked), but we could add an assertion

@odersky odersky closed this as completed Jan 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants