1
votes

I want to narrow a type when I'm pattern matching in order to special case it, but when trying to bind the tuple I'm pattern matching against to a variable, the Scala compiler seems to lose the type narrowing performed in the match. This prevents me from just forwarding the match and instead I have to recreate the tuple so that the types are preserved.

Minified example:

sealed trait Fruit
case class Apple(weight: Int = 200) extends Fruit
case object Orange extends Fruit

object PatternBinding extends App {
  def handleApple(appleWithIndex: (Apple, Int)) =
    println(s"Apple with weight: ${appleWithIndex._1}")

  def handleFruit(fruitWithIndex: (Fruit, Int)) =
    println("Other fruit")

  val seq = Seq[Fruit](Apple(120), Orange, Orange, Apple())

  seq.zipWithIndex.foreach {
    case tup @ (apple: Apple, index) =>
      handleApple(tup) // <------------ does not compile
//      handleApple((apple, index)) // compiles
    case tup @ (fruit, index) => handleFruit(tup)
  }
}

Is there some reason for this or just a peculiar corner case?

Is there an existing issue which I may track? I tried searching, but came up empty handed.

The compiler error message:

    type mismatch;  found   : (Fruit, Int)  required: (Apple, Int)

In essence I would have expected the Apple case to be transformed in to something like this pseudo-scala:

case tup : (Apple, Int) @ (apple: Apple, index) =>

With tup having the type (Apple, Int).

Scala fiddle example: http://scalafiddle.net/console/f182c4c2e7b4bd91debd2d0d636becac

Scala version is 2.11.6.

2

2 Answers

2
votes

The spec says the type params are instantiated to conform to the expected type of the pattern. That's how you know in turn what the expected types of the nested "component patterns" are.

So it's outside-in, not inside-out.

I wonder if tuple optimization could work for this case.

The issue says, If I create a tuple just to deconstruct it, don't bother constructing it.

This would add, if I deconstruct a tuple just to construct it, just reuse the instance, even though it's a different tuple type.

In other words, avoid reboxing the int and the tuple for (x,i):

scala> :pa
// Entering paste mode (ctrl-D to finish)

sealed trait Fruit
case class Apple(weight: Int = 200) extends Fruit
case object Orange extends Fruit
val fruits = Seq[Fruit](Apple(120), Orange, Orange, Apple())

// Exiting paste mode, now interpreting.

defined trait Fruit
defined class Apple
defined object Orange
fruits: Seq[Fruit] = List(Apple(120), Orange, Orange, Apple(200))

scala> fruits.zipWithIndex map { case (x: Apple, i: Int) => (x,i) case _  => (null, -1) }
res0: Seq[(Apple, Int)] = List((Apple(120),0), (null,-1), (null,-1), (Apple(200),3))

I appended the use case to the ticket, since tuples are special creatures and will be final soon.

0
votes

I guess, that apple: Apple is just a test that checks apple.isInstanceOf[Apple] and assigns the value to the apple variable. However, it doesn't change the type of tup which is Pair[Fruit, Int] (or, equivalently, (Fruit, Int)). (Additional traits Product with Serializable come from the keyword case in declaration of Orange and Apple.)

(Actually, Seq(Apple(120), Orange, Orange, Apple()) has the type Seq[T] where T is the lowest super type of the sequence elements. That super type T contains Fruit of course. However, the keyword case adds additional types Product with Serializable automatically to both Apple and Orange, but not to Fruit. That's why the calculated type T has some more type information. If you wish, you may remove the exhaustive types by ascribing type of an element of the sequence: Seq(Apple(120):Fruit, Orange, Orange, Apple()). It will have type Seq[Fruit].).