Type inference with pattern binder at sing @ in match case doesn't work as expected

Viewed 122

Suppose Lofty is a sealed trait and Earthy is one of its case classes. In a match such as this:

loftyItem match {
...
 case e @ Earthy(x,y,z) => { foo(e) }
...
}

where foo expects an Earthy as argument, the compiler melts down because e is inferred only to be of type Lofty. I can work around this, but it doesn't fit my model of how things ought to be. (I'm working with Scala 2.13.5.) Can someone explain why the compiler's behavior makes sense, and make me happy with Scala again?

In response to comments, let me be more precise:

object QTest {
  trait Ethereal

  case class Lofty[A <: Ethereal](val slotMaybe: Option[A]) {
  }

  class Earthy(val field: Int) extends Ethereal

  object Earthy {
    def apply(fx: Int): Earthy = {
      new Earthy(fx)
    }

    def unapply(x: Ethereal): Option[Int] = x match {
      case y: Earthy => Some(y.field)
      case _ => None
    }
  }

  def testReally(just: Lofty[Ethereal]):
      Lofty[Earthy] = {
    just.slotMaybe match {
      case Some(c) => c match {
        case cnfC @ Earthy(i) => {
          just.copy(slotMaybe = Some(cnfC))
        }
        case _ => throw new RuntimeException("oops")
      }
      case _ => throw new RuntimeException("oops")
    }
  }
}

Compiling this produces the error:

QTest.scala:25: error: type mismatch;
 found   : QTest.Ethereal
 required: QTest.Earthy
          just.copy(slotMaybe = Some(cnfC))

I obviously jumped to a conclusion, but the full example seems to have the same issue. Why does the compiler infer type Ethereal for cnfC instead of Earthy? Even if the compiler gets the type right for most uses of @, why does it get it wrong here?

1 Answers

SLS 8.1.3 Pattern Binders states

A pattern p implies a type T if the pattern matches only values of the type T.

The pattern Earthy(i) in

case cnfC @ Earthy(i) =>

represents extractor pattern meaning it will match according to your definition of unapply which is

object Earthy {
  def unapply(x: Ethereal): Option[Int] = ???
}

Because the declared type of x is wider Ethereal instead of narrower Earthy it will not match

... only values of the type T

where T = Earthy, but instead it can match other subtypes of Ethereal as well. Hence the compiler can only be sure it will be some Ethereal.

If you wish to make it compile with the extractor pattern then either declare your unapply as

object Earthy {
  def unapply(x: Earthy): Option[Int] = ???
}

or better yet use case class instead which gets a correct unapply automagically

case class Earthy(field: Int) extends Ethereal
Related