Классы абстрагирования

Я изучаю способы абстрагирования Case-классов в Scala. Например, вот попытка Either[Int, String] (используя Scala 2.10.0-M1 и -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]
}

Учитывая это определение, я мог бы написать что-то вроде этого:

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)
  }
}

Вот первая реализация для модуля, где представление для Either это 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) }
  }
}

unapplyс Left а также Right действительно эксклюзив, поэтому следующие работы ожидаемые:

scala> foo(M1)("42")
it's an int: 42

scala> foo(M1)("quarante-deux")
it's a string: quarante-deux

Все идет нормально. Моя вторая попытка заключается в использовании scala.Either[Int, String] как естественная реализация для 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)
  }
}

Но это не работает, как ожидалось:

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

Есть ли способ получить правильный результат?

1 ответ

Решение

Проблема в этом сопоставителе:

intOrString match {
    case m.Left(i) => println("it's an int: "+i)
    case m.Right(s) => println("it's a string: "+s)
}

Безоговорочно выполняет m.Left.unapply на intOrString, О том, почему это так, смотрите ниже.

Когда вы звоните foo(M2)(Right("quarante-deux")) вот что происходит:

  • m.Left.unapply решает в M2.Left.unapply что на самом деле scala.Left.unapply
  • intOrString является Right("quarante-deux")

Как следствие, scala.Left.unapply называется на Right("quarante-deux") который вызывает CCE.

Теперь, почему это происходит. Когда я попытался запустить ваш код через интерпретатор, я получил следующие предупреждения:

<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)
                   ^

unapply метод ApplyAndUnApply стирается в Option unapply(Object), Поскольку невозможно запустить что-то вроде intOrString instanceof m.Left (так как m.Left тоже стирается), компилятор компилирует это совпадение для запуска всех стертых unapplys.

Ниже приведен один из способов получить правильный результат (не уверен, что он согласуется с вашей первоначальной идеей абстрагирования классов case):

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
    }
}
Другие вопросы по тегам