class: center, middle # How to totally fix PartialFunction Dale Wijnand (
@dwijnand
) Scala World, 3rd Sept 2019
??? * I work for Lightbend * I've been a maintainer of sbt for many years * More recently I've moved to the "application space", helping maintain Play and Lagom * I also maintain sbt-extras, sbt-dynver, sbt-travisci, sbt-project-graph, and MiMa * I also proposed and implement trailing commas in Scala 2.12.2 * Thank you for coming to my talk * I also recommend you watch Jorge's Bloop talk, when the recording is out * How many of you have experience as full-time or part-time Scala programmers? * Keep your hands up if you have experience authoring and/or maintaining a Scala library? * Keep your hands up if you use MiMa? * How many of you know of the shortcomings of PF? --- # Agenda 1. Definitions 2. Usage 3. Implementation 4. Problem 5. Proposal 6. Summary --- # Definitions Let's start with `Function1`. How is `Function1` defined? ```scala trait Function1[-A, +B] { def apply(a: A): B } ``` -- But it's more commonly known by its infix type alias: ```scala type A => B = Function1[A, B] // =>[A, B] ``` -- Which, by the way, is special because it's right-associative: ```scala A => B => C // means A => (B => C) ``` --- # Definitions And what is the definition of `PartialFunction`? ```scala trait PartialFunction[-A, +B] extends (A => B) { def isDefinedAt(a: A): Boolean } ``` -- I often type alias it. Ideally I'd like: ```scala type A ?=> B = PartialFunction[A, B] ``` -- (in the past I've also used this alternative:) ```scala type A =?> B = PartialFunction[A, B] ``` -- But to make it right-associative, like `=>`, I'll use: ```scala type A ?=>: B = PartialFunction[A, B] ``` --- # Definitions `Function1` then has some utility methods (combinators): ```scala trait Function1[-A, +B] { def andThen[C](g: B => C): A => C = a => g(apply(a)) def compose[R](g: R => A): R => B = r => apply(g(r)) } ``` --- # Definitions ... and `PartialFunction` has a lot more methods: ```scala trait PartialFunction[-A, +B] extends (A => B) { def andThen[C](g: B => C): A ?=>: C def andThen[C](g: B ?=>: C): A ?=>: C def compose[R](g: R ?=>: A): R ?=>: B def orElse[A1 <: A, B1 >: B](g: A1 ?=>: B1): A1 ?=>: B1 def lift: A => Option[B] // and `.unlift` to go back def unapply(a: A): Option[B] = lift(a) def elementWise = new ElementWiseExtractor(this) def applyOrElse[A1 <: A, B1 >: B](a: A1, default: A1 => B1) = { if (isDefinedAt(a)) apply(a) else default(a) } def runWith[U](action: B => U): A => Boolean = { (a: A) => { val b: B = applyOrElse(a, checkFallback[B]) if (!fallbackOccurred(b)) { action(b) true } else false } } } ``` --- # Definitions There are also utility methods on `PartialFunction`'s companion object: ```scala object PartialFunction { def empty[A, B]: A ?=>: B def fromFunction[A, B](f: A => B): A ?=>: B = { case a => f(a) } def cond[A](a: A)(pf: A ?=>: Boolean): Boolean = pf.applyOrElse(a, constFalse) def condOpt[A, B](a: A)(pf: A ?=>: B]): Option[B] = pf.lift(a) private val fallback_fn: Any => Any = _ => fallback_fn private def checkFallback[B]: Any => B = fallback_fn.asInstanceOf[Any => B] private def fallbackOccurred[B](b: B): Boolean = fallback_fn.eq(b.asInstanceOf[AnyRef]) } ``` --- # Usage So when do you want to use `PartialFunction`? -- 1) When you want to: * operate on only part of the input * without addition runtime costs (`Option` boxing). -- For example, mapping on only part of the elements: ```scala xs.collect { case s if s.startsWith("FOO_") => ... } ``` -- or handling only part of the actor messages: ```scala def receive = { case Deposit(amt) => ??? case Withdraw(amt) => ??? // missing: CheckBalance } ``` --- # Usage 2) When you want to define individual partial functions and then combine them with `orElse`: ```scala myFuture.recoverWith( onNoUser.orElse(onNoToken).orElse(recordIncident) ) ``` --- # Implementation So given an expression like: ```scala param => selector match { cases } ``` what's the resulting `PartialFunction`? -- ```scala ... extends AbstractPartialFunction[..., ...] { // actually defined in AbstractPartialFunction def apply(x: A): B = applyOrElse(x, PartialFunction.empty) def isDefinedAt(a: A): Boolean = $selector match { $casesTrue; case _ => false } def applyOrElse[A1 <: A, B1 >: B](a: A1, default: A1 => B1) = $selector match { $cases; case _ => default(x) } } ``` --- # Problem So what problem does `PartialFunction` have? -- ```scala trait Function1[-A, +B] { def apply(a: A): B } trait PartialFunction[-A, +B] extends (A => B) { def isDefinedAt(a: A): Boolean def apply(a: A): B } ``` -- `PartialFunction` _changes the expectations of `apply`_ -- I believe `PartialFunction` breaks Liskov Substitution Principle. ??? The counter is from an object-oriented point-of-view `PartialFunction` adds `isDefinedAt` And therefore it makes sense that `PartialFunction` extends `Function1` (Perhaps rename it `QueryableFunction`) Now, there are no totality guarantees in Scala There's no guarantee that `Function1` doesn't throw But there is an _assumption_ that a `A => B` is total Or at least defined for "most" values `PartialFunction`'s addition of `isDefinedAt` along with the ergonomic syntax for defining them changes the expectations of `apply` Suddenly it's _more likely_ that `apply` isn't defined --- # Problem Here's an example of such an expectation: ```scala scala> List("FOO_A", "BAR_B").map { | case s if s.startsWith("FOO_") => s | } scala.MatchError: BAR_B (of class java.lang.String) at .$anonfun$res0$1(
:1) at scala.collection.immutable.List.map(List.scala:226) ... 58 elided ``` --- # Problem To put it another way: Given a value of type `Function1[..., ...]` * Is calling `apply` safe? * Should I check `isInstanceOf[PartialFunction[_, _]]`? * Should I just `try`/`catch` it? --- # Problem There's one additional (much smaller) problem: -- ```scala scala> Try[Int](???).recover(_ => 1) ^ error: type mismatch; found : Throwable => Int required: PartialFunction[Throwable,?] scala> Try[Int](???).recover { case _ => 1 } res1: scala.util.Try[Int] = Success(1) ``` -- I've fixed that one, see [scala/scala#8172][] "Synthesize a PartialFunction from function literal" [scala/scala#8172]: https://github.com/scala/scala/pull/8172 -- Coming in Scala 2.13.1 --- # Proposal So, actually, I have 3 proposals. --- # Proposal 1: The optimal solution 2 changes: 1. Make `Function1 extends PartialFunction` -- 2. Split `Function1`'s `apply` from `PartialFunction`'s `apply` (renamed `unsafeApply`) -- ```scala trait PartialFunction[-A, +B] { def isDefinedAt(a: A): Boolean /** Call ONLY if `isDefinedAt` is `true` for `a`! */ def unsafeApply(a: A): B def applyOrElse[A1 <: A, B1 >: B](a: A1, default: A1 => B1) = if (isDefinedAt(a)) unsafeApply(a) else default(a) } trait Function1[-A, +B] extends PartialFunction[A, B] { final def isDefinedAt(a: A) = true final def apply(a: A): B = unsafeApply(a) } ``` -- Note how `Function1` is still a SAM type. ??? With that implementation everything gets simpler You can pass `Function1` values where `PartialFunction` values are expected And the combinators (`andThen`, `orElse`, etc) are much simpler --- # Proposal 1: The optimal solution But the migration path is probably too complicate / not worth it. -- My first idea was this: 1. introduce a new "partial function" as a super-type of `Function1` -- 2. eventually deprecate `PartialFunction` (and just lose the name "PartialFunction") -- ```scala trait PF[-A, +B] { // real name, but with the ?=> alias def isDefinedAt(a: A): Boolean def unsafeApply(a: A): B def applyOrElse[A1 <: A, B1 >: B](a: A1, default: A1 => B1) = if (isDefinedAt(a)) unsafeApply(a) else default(a) } trait Function1[-A, +B] extends (A ?=> B) { final def isDefinedAt(a: A) = true final def unsafeApply(a: A) = apply(a) } trait PartialFunction[-A, +B] extends (A => B) ``` ??? and then what? In some version of Scala `{ case ... => ... }` generates a `PF` instead of `PartialFunction`? :-/ --- # Proposal 2: The more likely solution Just drop the inheritance: -- ```scala trait PartialFunction[-A, +B] { def isDefinedAt(a: A): Boolean /** Call ONLY if `isDefinedAt` is `true` for `a`! */ def unsafeApply(a: A): B @deprecated("Use unsafeApply", "3.0.0") def apply(a: A): B = unsafeApply(a) } trait Function1[-A, +B] { final def apply(a: A): B } ``` ??? So introduce `unsafeApply` on `PartialFunction` and `@deprecate` `apply` --- # Proposal 2: The more likely solution Maybe add a converter to "upcast": ```scala trait Function1[-A, +B] { ... def asPartial: A ?=>: B } ``` -- ```scala Try[Int](???).recover(fallback.asPartial) ``` -- But you can also use eta-expansion: ```scala Try[Int](???).recover(fallback(_)) ``` -- Or you can just mix-in both traits (if you have many instances) --- # Proposal 3: Or maybe... The last idea is... -- What if we actually had an unboxed `Option` in the standard library? ```scala type Opt[+A] >: Opt.None.type <: AnyRef ``` -- Could we redefine `PartialFunction` with it and `Function1`? ```scala type ?=>[-A, +B] = A => Opt[B] given [A, B] (f: A ?=> B) { def andThen[C](g: B => C): A ?=> C def andThen[C](g: B ?=> C): A ?=> C def compose[R](g: R ?=> A): R ?=> B def orElse[A1 <: A, B1 >: B](g: A1 ?=> B1): A1 ?=> B1 } ``` -- Maybe. --- # Summary 1. Proposal 1: `Function1 extends PF` -- 2. Proposal 2: distinct `Function1` and `PartialFunction` types -- 3. Proposal 3: introduce `Opt` and compose with `Function1` -- What do you prefer? -- Any questions or comments?