Thou whoreson zed! thou unnecessary letter!
- King Lear, 2.2.61
Partial functions are great in theory. By "great in theory", I mean that the basic idea is pretty simple but with very little additional research you can make weighty pronouncements involving the words "in" and "theory", as in, "in category theory, something something surjective algebra manifold."
The simplest example of a partial function is something like the logarithm, which isn't defined for arguments less than or equal to zero. Of course, it's also not defined for arguments that are strings or colors or egg-laying mammals, a restriction we usually discuss in terms of types. And one can discuss a function's domain in terms of type, but we usually choose not to.
In Scala, when you write a normal function, like d: Double => Math.log(d)
, you get a Function
, which is just a class that happens to have an apply
method
object loggy extends Function1[Double,Double] {
def apply(d: Double): Double = Math.log(d)
}
and a PartialFunction
is just like this, except you can ask where it is defined:
object ploggy extends PartialFunction[Double,Double] {
def apply(d: Double): Double = Math.log(d)
def isDefinedAt(d: Double): Boolean = d > 0.0
}
If you just call ploggy(0.0)
, you'll get the usual floating point exception, but Scala gives you other, spiffier ways to invoke it, such as
ploggy.applyOrElse(0.0, 42*_)
where we've taken the small liberty of assuming that if you passed a negative argument to our log function, you probably meant to multiply by 42 instead, and
(ploggy orElse anotherPartialFunction)(0.0)
where our backup function is also partial.
Those formulations are not commonly used. More popular is the collect
combinator
Seq(-2.0,1.0).collect(ploggy)
which combines a filter
on .isDefinedAt
and map
over what's left, yielding Seq(0.0)
in this case. It is even more common to see collect
used with a match-like syntax
Some(0).collect {
case 1 => "Hello"
case 2 => "Goodbye"
}
here applied to an Option
rather than a sequence and in this case expected to return None
.
If we ask politely,
scala -Xprint:typer -e 'Some(1).collect { case 1 => "Hello"; case 2 => "Goodbye"}'
Scala will reveal the trained hippopotamus behind the curtain:
...
scala.Some.apply[Int](1).collect[String](({
@SerialVersionUID(value = 0) final <synthetic> class $anonfun extends scala.runtime.AbstractPartialFunction[Int,String] with Serializable {
def <init>(): <$anon: Int => String> = {
$anonfun.super.<init>();
()
};
final override def applyOrElse[A1 <: Int, B1 >: String](x1: A1, default: A1 => B1): B1 = ((x1.asInstanceOf[Int]: Int): Int @unchecked) match {
case 1 => "Hello"
case 2 => "Goodbye"
case (defaultCase$ @ _) => default.apply(x1)
};
final def isDefinedAt(x1: Int): Boolean = ((x1.asInstanceOf[Int]: Int): Int @unchecked) match {
case 1 => true
case 2 => true
case (defaultCase$ @ _) => false
}
};
new $anonfun()
}: PartialFunction[Int,String]))
};
...
This is no ordinary hippopotamus. The scala typer has divined the presence of a match block where a PartialFunction
is expected
case Match(sel, cases) if (sel ne EmptyTree) && (pt.typeSymbol == PartialFunctionClass) => ...
and replicated our innocuous looking switch clause into two different methods of a brand new class definition. It feels imbalanced,
a conflation of concerns, that the central typing logic of the compiler is in the business of extending one very specific class in
scala-library.jar
.
Oh Snozzcumbers
‘It’s disgusterous!, the BFG gurgled. ‘It’s sickable! It’s
rotsome! It’s maggotwise! Try it yourself, this foulsome
partial function!'
‘No, thank you,‘ Sophie said, backing away. ‘It’s all you’re
going to be guzzling around here from now on so you might as well
get used to it,’ said the BFG. ‘Go on, you snipsy little winkle,
have a go!’
Sophie took a small nibble. ‘Ugggggggh!’ she spluttered. ‘Oh no!
Oh gosh! Oh help!’ She spat it out quickly. ‘It tastes of
frogskins!’ she gasped. ‘And rotten fish!’
‘Worse than that!’ cried the BFG, roaring with laughter. ‘To me it
is tasting of clockcoaches and slime-wanglers!’
And yet Dahl was writing in the context of an alternative to cannibalism, while partial functions are an alternative to maybe... well, an alternative to
Maybe
, or Option
as we Scalactites call it. Some(1).flatMap{case 1 => Some("Hello"); case 2 => Some("Goodbye"); case _ => None}
emerges
from the typer without significant change:
scala.Some.apply[Int](1).flatMap[String](((x0$1: Int) => x0$1 match {
case 1 => scala.Some.apply[String]("Hello")
case 2 => scala.Some.apply[String]("Goodbye")
case _ => scala.None
}))
I'm not being entirely fair, because the lambda is going to get turned into a Function1
during the uncurry phase,
scala -Xprint:uncurry -e 'Some(1).flatMap {case 1 => Some("Hello"); case 2 => Some("Goodbye"); case _ => None}'
Some[Int](1).flatMap[String]({
@SerialVersionUID(value = 0) final <synthetic> class $anonfun extends scala.runtime.AbstractFunction1[Int,Option[String]] with Serializable {
def <init>(): <$anon: Int => Option[String]> = {
$anonfun.super.<init>();
()
};
final def apply(x0$1: Int): Option[String] = {
case <synthetic> val x1: Int = x0$1;
x1 match {
case 1 => new Some[String]("Hello")
case 2 => new Some[String]("Goodbye")
case _ => scala.None
}
}
};
(new <$anon: Int => Option[String]>(): Int => Option[String])
})
While we're spared gratuitous doubling of the core logic, we still get
a bespoke .class
file and constructor.
Lambda functions are the new normal
But not for long! With
Scala 2.12 and Java 1.8, lambda functions will be implemented with
invoke-dynamic. Under 2.11, we can get a taste of this advanced
feature with -Ydelambdafy:method
,
scala -Ydelambdafy:method -Xprint:uncurry -e 'Some(1).flatMap {case 1 => Some("Hello"); case 2 => Some("Goodbye"); case _ => None}'
Some[Int](1).flatMap[String]({
final <artifact> def $anonfun(x0$1: Int): Option[String] = {
case <synthetic> val x1: Int = x0$1;
x1 match {
case 1 => new Some[String]("Hello")
case 2 => new Some[String]("Goodbye")
case _ => scala.None
}
};
((x0$1: Int) => $anonfun(x0$1))
})
which, again, doesn't look that much different from the original. Now that lambda functions have gone mainstream, now that Brian Goetz has decreed that they should be pretty and efficient, Scala doesn't have to try so hard to fake them. Specifically, the obvious way to fake them as been hardwired into the JVM - Single Method Interfaces are a thing. Unfortunately, Double Method Interfaces have not attained thing status, and there's no reason to think they will, because that would require convincing Brian Goetz that partial functions should be pretty and efficient, and we all know that partial functions are disgusting and unnecessary.
Where, then the idea come from? Not from Haskell, where the main guidance is on how to avoid them. Not from Python, where - bless their hearts - they seem to have confused the idea with currying. Not Idris, whose dependent type system largely avoids the problem by checking the domain at compile time. We have something over Idris, in that Scala could deal better with - I don't know - a function defined only where the Riemann zeta function is less than 1/2.
(Pause briefly while you imagine me hunting for 30 minutes for just the right illustration and then deciding it was silly.)
TL;DR
When you're tempted to write a partial function, write a function that returns Option
instead.
Here's what Chaucer has to say on the subject:
O blisful God, that art so Iust and trewe!
Lo, how that thou biwreyest mordre alway!
Parcial fonctione wol out, that see we day by day.
Parcial fonctione is so wlatsom and abhominable
To God, that is so Iust and resonable,
- The Nun's Priest's Tale
Comments
comments powered by Disqus