Search code examples
scalapattern-matchingtype-safety

Is it a type checker bug?


I narrowed it down to the following piece of code:

trait A[T] {
  def apply(t: T): Int
}

sealed trait P {
  def apply(): Int
}

case class I[T](a: A[T], t: T) extends P {
  def apply: Int = a(t)
}

case class X[T1, T2](a1: A[T1], a2: A[T2]) extends A[(T1, T2)] {
  def apply(t: (T1, T2)): Int =
    t match {
      case (t1, t2) => a1(t1) + a2(t2)
    }
}

object m {
  def apply(p1: P, p2: P): P =
    (p1, p2) match {
      case (I(a1, t1), I(a2, t2)) =>
        I(X(a1, a2), (t2, t1)) // <-- Here
    }
}

As you can see, I have a type error in the line marked <-- Here. And yet, the code compiles without even a warning, and fails with ClassCastException at runtime. Code to play with:

case class E() extends A[Int] {
  def apply(t: Int): Int = t
}

case class S() extends A[String] {
  def apply(t: String): Int = t.length
}

object Test {
  def apply() = {
    val pe: P = I(E(), 3)
    val ps: P = I(S(), "abcd")
    val pp: P = m(pe, ps)
    pp()
  }
}

I know that when pattern-matching scala sometimes can't check that a value is of the right type, but that usually results in compiler warning.

So, is it a bug, or do I miss something?

Update: What I'm worried about is that I can make a type error and the compiler won't even warn me. I do understand that (t1, t2) is the correct order; but if I write it incorrectly, I won't discover it until executing the program, and maybe even later, although it's clearly a type error.


Solution

  • Maybe the lack of warning is related to this one:

    https://issues.scala-lang.org/browse/SI-9188

    It doesn't seem to do anything useful with the type parameter on A, unless it can prove statically that you got it wrong.

    The last match here should warn:

    scala> val i = I(E(), 42)
    i: I[Int] = I(E(),42)
    
    scala> i match { case I(a: A[Int], x) => }
    
    scala> i match { case I(a: A[String], x) => }
    <console>:15: warning: non-variable type argument String in type pattern A[String] is unchecked since it is eliminated by erasure
                  i match { case I(a: A[String], x) => }
                                      ^
    <console>:15: error: pattern type is incompatible with expected type;
     found   : A[String]
     required: A[Int]
                  i match { case I(a: A[String], x) => }
                                      ^
    
    scala> (i: P) match { case I(a: A[String], x) => }
    <console>:15: warning: non-variable type argument String in type pattern A[String] is unchecked since it is eliminated by erasure
                  (i: P) match { case I(a: A[String], x) => }
                                           ^
    <console>:15: error: pattern type is incompatible with expected type;
     found   : A[String]
     required: A[Any]
    Note: String <: Any, but trait A is invariant in type T.
    You may wish to define T as +T instead. (SLS 4.5)
                  (i: P) match { case I(a: A[String], x) => }
                                           ^
    
    scala> (i: P) match { case I(a: A[Int], x) => }
    <console>:15: warning: non-variable type argument Int in type pattern A[Int] is unchecked since it is eliminated by erasure
                  (i: P) match { case I(a: A[Int], x) => }
                                           ^
    <console>:15: error: pattern type is incompatible with expected type;
     found   : A[Int]
     required: A[Any]
    Note: Int <: Any, but trait A is invariant in type T.
    You may wish to define T as +T instead. (SLS 4.5)
                  (i: P) match { case I(a: A[Int], x) => }
                                           ^
    
    scala> (i: P) match { case I(a: A[_], x) => }
    
    scala> (i: P) match { case I(a: A[Any], x) => }
    

    Just to add:

    scala> (i: P) match { case I(a: A[Any], x) => a("foo") }
    java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
      at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:105)
      at E.apply(<console>:33)
      ... 33 elided