I'm exploring ways to abstract Case Classes in Scala. For example, here is an attempt for Either[Int, String]
(using Scala 2.10.0-M1 and -Yvirtpatmat
):
trait ApplyAndUnApply[T, R] extends Function1[T, R] {
def unapply(r: R): Option[T]
}
trait Module {
type EitherIntOrString
type Left <: EitherIntOrString
type Right <: EitherIntOrString
val Left: ApplyAndUnApply[Int, Left]
val Right: ApplyAndUnApply[String, Right]
}
Given this definition, I could write something like that:
def foo[M <: Module](m: M)(intOrString: m.EitherIntOrString): Unit = {
intOrString match {
case m.Left(i) => println("it's an int: "+i)
case m.Right(s) => println("it's a string: "+s)
}
}
Here is a first implementation for the module, where the representation for the Either
is a String
:
object M1 extends Module {
type EitherIntOrString = String
type Left = String
type Right = String
object Left extends ApplyAndUnApply[Int, Left] {
def apply(i: Int) = i.toString
def unapply(l: Left) = try { Some(l.toInt) } catch { case e: NumberFormatException => None }
}
object Right extends ApplyAndUnApply[String, Right] {
def apply(s: String) = s
def unapply(r: Right) = try { r.toInt; None } catch { case e: NumberFormatException => Some(r) }
}
}
The unapply
s make the Left
and Right
really exclusive, so the following works as expecting:
scala> foo(M1)("42")
it's an int: 42
scala> foo(M1)("quarante-deux")
it's a string: quarante-deux
So far so good. My second attempt is to use scala.Either[Int, String]
as the natural implementation for Module.EitherIntOrString
:
object M2 extends Module {
type EitherIntOrString = Either[Int, String]
type Left = scala.Left[Int, String]
type Right = scala.Right[Int, String]
object Left extends ApplyAndUnApply[Int, Left] {
def apply(i: Int) = scala.Left(i)
def unapply(l: Left) = scala.Left.unapply(l)
}
object Right extends ApplyAndUnApply[String, Right] {
def apply(s: String) = scala.Right(s)
def unapply(r: Right) = scala.Right.unapply(r)
}
}
But this does not work as expected:
scala> foo(M2)(Left(42))
it's an int: 42
scala> foo(M2)(Right("quarante-deux"))
java.lang.ClassCastException: scala.Right cannot be cast to scala.Left
Is there a way to get the right result?
The problem is in this matcher:
It unconditionally executes
m.Left.unapply
on theintOrString
. As to why it does, see below.When you call
foo(M2)(Right("quarante-deux"))
this is what is happening:m.Left.unapply
resolves toM2.Left.unapply
which is in factscala.Left.unapply
intOrString
isRight("quarante-deux")
Consequently,
scala.Left.unapply
is called onRight("quarante-deux")
which causes CCE.Now, why this happens. When I tried to run your code through the interpreter, I got these warnings:
The
unapply
method ofApplyAndUnApply
gets erased toOption unapply(Object)
. Since it's impossible to run something likeintOrString instanceof m.Left
(becausem.Left
is erased too), the compiler compiles this match to run all erasedunapply
s.One way to get the right result is below(not sure if it goes along with your original idea of abstracting case classes):