Abstracting Case Classes
-
28-04-2021 - |
Question
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?
Solution
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 toM2.Left.unapply
which is in factscala.Left.unapply
intOrString
isRight("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 unapply
s.
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
}
}