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 unapplys 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.unapplyon theintOrString. As to why it does, see below.When you call
foo(M2)(Right("quarante-deux"))this is what is happening:m.Left.unapplyresolves toM2.Left.unapplywhich is in factscala.Left.unapplyintOrStringisRight("quarante-deux")Consequently,
scala.Left.unapplyis 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
unapplymethod ofApplyAndUnApplygets erased toOption unapply(Object). Since it's impossible to run something likeintOrString instanceof m.Left(becausem.Leftis erased too), the compiler compiles this match to run all erasedunapplys.One way to get the right result is below(not sure if it goes along with your original idea of abstracting case classes):