Abstracting Case Classes

297 views Asked by At

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?

1

There are 1 answers

1
Fixpoint On BEST ANSWER

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.unapply
  • intOrString 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
    }
}