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:
intOrString match {
case m.Left(i) => println("it's an int: "+i)
case m.Right(s) => println("it's a string: "+s)
}
It unconditionally executes m.Left.unapply on the intOrString. As to why it does, see below.
When you call foo(M2)(Right("quarante-deux")) this is what is happening:
m.Left.unapply resolves to M2.Left.unapply which is in fact scala.Left.unapplyintOrString is Right("quarante-deux")Consequently, scala.Left.unapply is called on Right("quarante-deux") which causes CCE.
Now, why this happens. When I tried to run your code through the interpreter, I got these warnings:
<console>:21: warning: abstract type m.Left in type pattern m.Left is unchecked since it is eliminated by erasure
case m.Left(i) => println("it's an int: "+i)
^
<console>:22: warning: abstract type m.Right in type pattern m.Right is unchecked since it is eliminated by erasure
case m.Right(s) => println("it's a string: "+s)
^
The unapply method of ApplyAndUnApply gets erased to Option unapply(Object). Since it's impossible to run something like intOrString instanceof m.Left (because m.Left is erased too), the compiler compiles this match to run all erased unapplys.
One way to get the right result is below(not sure if it goes along with your original idea of abstracting case classes):
trait Module {
type EitherIntOrString
type Left <: EitherIntOrString
type Right <: EitherIntOrString
val L: ApplyAndUnApply[Int, EitherIntOrString]
val R: ApplyAndUnApply[String, EitherIntOrString]
}
object M2 extends Module {
type EitherIntOrString = Either[Int, String]
type Left = scala.Left[Int, String]
type Right = scala.Right[Int, String]
object L extends ApplyAndUnApply[Int, EitherIntOrString] {
def apply(i: Int) = Left(i)
def unapply(l: EitherIntOrString) = if (l.isLeft) Left.unapply(l.asInstanceOf[Left]) else None
}
object R extends ApplyAndUnApply[String, EitherIntOrString] {
def apply(s: String) = Right(s)
def unapply(r: EitherIntOrString) = if (r.isRight) Right.unapply(r.asInstanceOf[Right]) else None
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With