qtwo
qtwo

Reputation: 507

Surprising PartialFunction literal

I just got puzzled by the fact that this partial function pf doesn't blow up with a MatchError when inner is not InnerA


sealed trait Inner
case class InnerA(name: String) extends Inner
case class InnerB(name: String, value: Int) extends Inner
case class Input(id: String, inner: Inner)

case class Output(id: String, inner: InnerA)

  val pf: PartialFunction[Input, Output] = { input =>
    input.inner match {
      case innerA: InnerA =>
        val Input(id, _) = input
        Output(id, innerA)
    }
  }

instead it is simply undefined so this passes

    Seq(
      Input("1", InnerA("a1")),
      Input("2", InnerB("b2", 2)),
      Input("3", InnerA("a3"))
    ).collect(pf) shouldBe Seq(
      Output("1", InnerA("a1")),
      Output("3", InnerA("a3"))
    )

If I add a line I get a compilation warning and trying to pass an InnerB in the collect above throws a MatchError (as I originally expected):

  val pf: PartialFunction[Input, Output] = { input =>
    println(input)
    input.inner match {
      case innerA: InnerA =>
        val Input(id, _) = input
        Output(id, innerA)
    }
  }
InnerB(b2,2) (of class casa.InnerB)
scala.MatchError: InnerB(b2,2) (of class casa.InnerB)

Why is this? Is this quirk documented somewhere?

(I'm using Scala 2.13.3)

Upvotes: 1

Views: 82

Answers (2)

Alexey Romanov
Alexey Romanov

Reputation: 170713

this partial function pf doesn't blow up with a MatchError when inner is not InnerA

If you call it directly with such an argument

pf(Input("a", InnerB("b", 0)))

you do get a MatchError; but the whole point of PartialFunction is to provide additional information in isDefinedAt, and collect uses it by not calling pf where isDefinedAt returns false.

Is this quirk documented somewhere?

See paragraph 6.23.1 Translation of specification:

When a PartialFunction is required, an additional member isDefinedAt is synthesized, which simply returns true. However, if the function literal has the shape x => x match { … }, then isDefinedAt is derived from the pattern match in the following way: each case from the match expression evaluates to true, and if there is no default case, a default case is added that evaluates to false.

So for your second version isDefinedAt is always true; the first one doesn't exactly fit x => x match..., but apparently it's supported too. Now the usual way to define a PartialFunction is like Luis Miguel Mejía Suárez's comment says

{ case Input(id, InnerA(name)) => Output(id, InnerA(name)) }

but it simply gets translated into

x => x match { case Input(id, InnerA(name)) => Output(id, InnerA(name)) }

Upvotes: 2

Rodrigo Vedovato
Rodrigo Vedovato

Reputation: 1008

What is happening is that, in the first case, the compiler is "removing" the input match from and using input.inner.

When I run scalac -Xprint:typer Test.scala, the first code turns into:

final override def applyOrElse[A1 <: Input, B1 >: Output](input: A1, default: A1 => B1): B1 = (input.inner: Inner @unchecked) match {
  case (innerA @ (_: InnerA)) => {
    val id: String = (input: A1 @unchecked) match {
      case (id: String, inner: Inner): Input((id @ _), _) => id
    };
    Output.apply(id, innerA)
  }
  case (defaultCase$ @ _) => default.apply(input)
};

final def isDefinedAt(input: Input): Boolean = (input.inner: Inner @unchecked) match {
  case (innerA @ (_: InnerA)) => true
  case (defaultCase$ @ _) => false
}

Which means that your function will behave like a PartialFunction[Inner, Output], so the compiler knows that it doesn't need to warn you that your match is not exhaustive.

On the other hand, when you see the results for the method with the print instruction, you get:

final override def applyOrElse[A1 <: Input, B1 >: Output](input: A1, default: A1 => B1): B1 = ((input.asInstanceOf[Input]: Input): Input @unchecked) match {
  case (defaultCase$ @ _) => {
    scala.Predef.println("xxx");
    input.inner match {
      case (innerA @ (_: InnerA)) => {
        val id: String = (input: A1 @unchecked) match {
          case (id: String, inner: Inner): Input((id @ _), _) => id
        };
        Output.apply(id, innerA)
      }
    }
  }
  case (defaultCase$ @ _) => default.apply(input)
};
final def isDefinedAt(input: Input): Boolean = ((input.asInstanceOf[Input]: Input): Input @unchecked) match {
  case (defaultCase$ @ _) => true
  case (defaultCase$ @ _) => false
}

In this case, you're creating a PartialFunction[Input, Output] that is defined for all intervals of Input, and this is fine. But when the compiler checks the inner input.inner match, it warns you that this match - not the first one - is not exhaustive.

Upvotes: 4

Related Questions