Search code examples
scalapattern-matchingtype-inferencescala-collectionsscala-compiler

Why collect with pattern match cannot narrow a specific class?


Let's consider following trait:

sealed trait AB
case class A(a: Int) extends AB
case class B(b: Int) extends AB

I am trying to collect to limit a collection to specific subclass.

If I try to collect, matching individual components and reassembling the tuple:

scala> Seq.empty[(Int, AB)].collect { case (id, a @ A(_))  => (id, a) } : Seq[(Int, A)]
res6: Seq[(Int, ab.A)] = List()

compiler is happy, but if try to return a full match:

scala> Seq.empty[(Int, AB)].collect { case x @ (_, A(_))  => x } : Seq[(Int, A)]

things get ugly:

<console>:27: error: type mismatch;
 found   : Seq[(Int, ab.AB)]
 required: Seq[(Int, ab.A)]
       Seq.empty[(Int, AB)].collect { case x @ (_, A(_))  => x } : Seq[(Int, A)]

Why Scala compiler is unable to handle the second case?


Solution

  • It seems that this is because the pattern matching proceeds in top-down fashion into the subpatterns, without passing any additional information from the subpatterns to the root pattern.

    When

    x @ (_, A(_))
    

    is matched, all x knows about the pattern is that it has the expected type (Int, AB), and this becomes the inferred type of x.

    But when you attach pattern variables i and a to the subpatterns, then more specific information can be extracted and saved in the inferred types of i and a. In this particular case with a @ A(_), the following paragraph in the spec seems relevant:

    A pattern binder x@p consists of a pattern variable x and a pattern p. The type of the variable x is the static type T of the pattern p.

    In the case of A(_), the static type can be inferred to be A just by looking at the top-level element of the pattern, without recursing into subpatterns. Therefore, the type of a is inferred to be A, the type of id is inferred to be Int, hence (id, a) is inferred to have type (Int, A).