Understanding PartialFunction trait with case


#1

Scala’s PartialFunction trait contains two abstract methods:isDefiendAt, apply.
Example implementation:

val divide = new PartialFunction[Int, Int] {
def apply(x: Int) = 42 / x
def isDefinedAt(x: Int) = x != 0
}

Another implementation is as follows:

val divide2: PartialFunction[Int, Int] = {
case d: Int if d != 0 => 42 / d
}

How does Scala maps second implementation to those two abstract methods?


#2

A partial function implemented via a (usually non-exhaustive) pattern match will use the pattern match for both methods.

  • isDefinedAt returns true, if a pattern matches.
  • apply simply executes the pattern match normally on the input

The compiler will also generate an efficient applyOrElse implementation, which avoids evaluating the pattern twice, once for checking definedness and once for applying.

Note that your two implementations are not identical: divide(0) will result in an ArithmeticException because of the zero division, while divide2(0) will result in a MatchError. In this case, where both throw an error, this may not be a critical difference, but if you e.g. had used Double instead of Int, your first implementation would return Infinity, while the second still would throw an error.
In most cases, where a PartialFunction is expected, the isDefinedAt is checked, but as PartialFunction is a subtype of Function in Scala, you should take care when passing it to methods expecting the latter.


#3

If you’re interested in the implementation details, again, javap holds all the answers.

C:\Users\marti\scala\indy>type test.scala
class Example {
  val partialExample: PartialFunction[Int, Int] = {
    case d: Int if guard(d) => processor(d)
  }
  def guard(i: Int) = i != 0
  def processor(i: Int) = 42 / 0
}

C:\Users\marti\scala\indy>scalac test.scala

C:\Users\marti\scala\indy>"C:\Program Files\Java\jdk1.8.0_191\bin\javap.exe" -c -p Example.class
Compiled from "test.scala"
public class Example {
  private final scala.PartialFunction<java.lang.Object, java.lang.Object> partialExample;

  public scala.PartialFunction<java.lang.Object, java.lang.Object> partialExample();
    Code:
       0: aload_0
       1: getfield      #17                 // Field partialExample:Lscala/PartialFunction;
       4: areturn

  public boolean guard(int);
    Code:
       0: iload_1
       1: iconst_0
       2: if_icmpeq     7
       5: iconst_1
       6: ireturn
       7: iconst_0
       8: ireturn

  public int processor(int);
    Code:
       0: bipush        42
       2: iconst_0
       3: idiv
       4: ireturn

  public Example();
    Code:
       0: aload_0
       1: invokespecial #29                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: new           #10                 // class Example$$anonfun$1
       8: dup
       9: aload_0
      10: invokespecial #32                 // Method Example$$anonfun$1."<init>":(LExample;)V
      13: putfield      #17                 // Field partialExample:Lscala/PartialFunction;
      16: return
}


"C:\Program Files\Java\jdk1.8.0_191\bin\javap.exe" -c -p Example$$anonfun$1.class
Compiled from "test.scala"
public final class Example$$anonfun$1 extends scala.runtime.AbstractPartialFunction$mcII$sp implements scala.Serializable {
  public static final long serialVersionUID;

  private final Example $outer;

  public final <A1, B1> B1 applyOrElse(A1, scala.Function1<A1, B1>);
    Code:
       0: iload_1
       1: istore        4
       3: iload         4
       5: istore        5
       7: aload_0
       8: getfield      #22                 // Field $outer:LExample;
      11: iload         5
      13: invokevirtual #26                 // Method Example.guard:(I)Z
      16: ifeq          35
      19: aload_0
      20: getfield      #22                 // Field $outer:LExample;
      23: iload         5
      25: invokevirtual #30                 // Method Example.processor:(I)I
      28: invokestatic  #36                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
      31: astore_3
      32: goto          46
      35: aload_2
      36: iload_1
      37: invokestatic  #36                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
      40: invokeinterface #42,  2           // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object;
      45: astore_3
      46: aload_3
      47: areturn

  public final boolean isDefinedAt(int);
    Code:
       0: iload_1
       1: istore_3
       2: iload_3
       3: istore        4
       5: aload_0
       6: getfield      #22                 // Field $outer:LExample;
       9: iload         4
      11: invokevirtual #26                 // Method Example.guard:(I)Z
      14: ifeq          22
      17: iconst_1
      18: istore_2
      19: goto          24
      22: iconst_0
      23: istore_2
      24: iload_2
      25: ireturn

  public final boolean isDefinedAt(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: invokestatic  #55                 // Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
       5: invokevirtual #57                 // Method isDefinedAt:(I)Z
       8: ireturn

  public final java.lang.Object applyOrElse(java.lang.Object, scala.Function1);
    Code:
       0: aload_0
       1: aload_1
       2: invokestatic  #55                 // Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
       5: aload_2
       6: invokevirtual #61                 // Method applyOrElse:(ILscala/Function1;)Ljava/lang/Object;
       9: areturn

  public Example$$anonfun$1(Example);
    Code:
       0: aload_1
       1: ifnonnull     6
       4: aconst_null
       5: athrow
       6: aload_0
       7: aload_1
       8: putfield      #22                 // Field $outer:LExample;
      11: aload_0
      12: invokespecial #66                 // Method scala/runtime/AbstractPartialFunction$mcII$sp."<init>":()V
      15: return
}

what you see here is that isDefinedAt calls the guard method (plus ceremony), but not the processor method.

Conspicuously absent is the Applymethod, which is just inherited from Function1, and just calls the entire thing (including guards).

That means that if (isDefinedAt(x)) apply(x) else doSomethingElse will call the guard twice, once for the isDefinedAt, and once for the apply

Do note there is also an applyOrElse method, that does the same thing, but will only evaluate the guard once.