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

StackOverflow https://stackoverflow.com/questions/20166468

  •  04-08-2022
  •  | 
  •  

Question

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?

Was it helpful?

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

OTHER TIPS

The reason why isDefinedAt on a PartialFunction produced by andThen returns inconsistent results is that it doesn't actually apply the first partial function to its argument, which could be an expensive operation.

This behaviour is likely to trip people up and isn't documented - you might want to submit a patch to add documentation for that.

P.S. My guess is that the reason for deepTest to behave as it does, is that the outermost match in the source code of a partial function definition, and only the outermost match, is considered for definedness purposes - but you'd have to check the source code of scalac to be sure, I think.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top