Search code examples
scalapartialfunction

Why is this PartialFunction defined but still crashes (correctly) after application in Scala?


I would like to try partial functions with a deep pattern matching use case. This initially (of course) didn't work after applying Some(Some(3)), but seemed defined instead :

def deepTest : PartialFunction [Option[Option[Int]], Int] = {
    case Some(v) => v match {
      case None => 3 
    }
    case None => 1
}

and I thought that by decoupling the nested pattern matching, things would be easier:

def deepTestLvl1 : PartialFunction [Option[Option[Int]], Option[Int]] = {
  case Some(v) => v
  case None => Some(1)
}


def deepTestLvl2 : PartialFunction [Option[Int], Int] = {
  case None => 3
}

but the result was the following:

scala> (deepTestLvl1 andThen deepTestLvl2) isDefinedAt(Some(Some(3)))
res24: Boolean = true

and after applying:

scala> (deepTestLvl1 andThen deepTestLvl2) (Some(Some(3)))
scala.MatchError: Some(3) (of class scala.Some)
    at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:248)
    at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:246)
    at $anonfun$deepTestLvl2$1.applyOrElse(<console>:7)
    at $anonfun$deepTestLvl2$1.applyOrElse(<console>:7)
        ....
    at scala.tools.nsc.MainGenericRunner.runTarget$1(MainGenericRunner.scala:83)
    at scala.tools.nsc.MainGenericRunner.process(MainGenericRunner.scala:96)
    at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:105)
    at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)

Am I doing something incorrectly? Shouldn't the isDefinedAt be called twice when I composed sequentially deepTestLvl{1,2} and give me the correct answer?


Solution

  • Very good question.

    Let's check the source and see what's happening under the covers:

    override def andThen[C](k: B => C): PartialFunction[A, C] =
      new AndThen[A, B, C] (this, k)
    

    We can observe here that andThen doesn't even expect a Partial Function, any Function that transforms the result will do. Your code works, because: trait PartialFunction[-A, +B] extends (A => B). This can actually be found in the documentation:

    def andThen[C](k: (B) ⇒ C): PartialFunction[A, C]

    Composes this partial function with a transformation function that gets applied to results of this partial function.

    C the result type of the transformation function.

    k the transformation function

    returns a partial function with the same domain as this partial function, which maps arguments x to k(this(x)).

    So there's currently no way to chain PartialFunctions in the way you would like, because as Robin said, it would require applying the function. Next to being computationally expensive it could also have side effects, which is a bigger problem.

    Update

    Whipped together an implementation you're looking for. Use it cautiously! As I already mentioned, if your code has side effects it will cause problems:

    implicit class PartialFunctionExtension[-A, B](pf: PartialFunction[A, B]) {
      def andThenPf[C](pf2: PartialFunction[B, C]) = new PfAndThen(pf, pf2)
    
      class PfAndThen[+C](pf: PartialFunction[A, B], nextPf: PartialFunction[B, C]) extends PartialFunction[A, C] {
        def isDefinedAt(x: A) = pf.isDefinedAt(x) && nextPf.isDefinedAt(pf.apply(x))
    
        def apply(x: A): C = nextPf(pf(x))
      }
    }
    

    Trying it out:

    deepTestLvl1.andThenPf(deepTestLvl2).isDefinedAt(Some(Some(3)))  // false
    deepTestLvl1.andThenPf(deepTestLvl2).isDefinedAt(Some(None))     // true
    deepTestLvl1.andThenPf(deepTestLvl2).apply(Some(None))           // 3