In the Scala Odersky book, he has an example explaining partial functions of page 295. It starts with this function:
val second: List[Int] => Int = {
case x :: y :: _ => y
}
So the above function will succeed if you pass it a three element list but not an empty list.
second(List(5,6,7))
works but not
second(List())
The above will throw a MatchError: List
Here is the part that is confusing to me. Odersky writes:
If you want to check whether a partial function is defined, you must first tell the compiler that you know you are working with partial functions.
Why would I want to check whether a partial function is defined. What is a partial function? Is it a function that only applies to some values?
The type List[Int] => Int includes all functions from lists of integers to integers, whether or not the functions are partial. The type that only includes partial functions from lists of integers to integers is written PartialFunction[List[Int], Int].
So the above function returns a function of type List[Int] => Int, I see that, but why do we need to change this function to type PartialFunction[List[Int], Int]
?
Here is the function redefined:
val second: PartialFunction[List [Int], Int] = {
case x :: y :: _ => y
}
I don't really get it. What is the benefit? Why do we want to check whether a partial function is defined? What does that even mean?
A partial function is any function, which takes only a single argument, that is defined (i.e. valid) only for a certain range of its argument's values. For example, Math.asin
is defined only for argument values in the range [-1.0, 1.0]
and is undefined for values outside of that range - so it is a partial function. For example, if we call Math.asin(5.0)
, we get NaN
returned, meaning that the function is not defined for that argument.
Note that a partial function doesn't necessarily have to throw an exception; it just needs to do something other than return a valid value.
A key principle of functional programming is referential transparency (RT), meaning that we should be able to replace an expression (such as a function call) with the value of that expression, without changing the meaning of the program. (For more on this topic, I highly recommend that you read Functional Programming in Scala by Chiusano and Bjarnason.) Clearly, that breaks down if an exception is thrown or if an invalid value is returned. For calls to partial functions to be referentially transparent, we can only call them with argument values for which they are defined, or we need to elegantly handle the undefined values. So how can we tell if a partial function is defined for some arbitrary argument value?
In Scala we can express partial functions as a subclass of scala.PartialFunction
that allows us to answer this question.
Let's look at your example in a Scala REPL session...
$ scala
Welcome to Scala 2.12.6 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_171).
Type in expressions for evaluation. Or try :help.
scala> val second: List[Int] => Int = {
| case x :: y :: _ => y
| }
<console>:11: warning: match may not be exhaustive.
It would fail on the following inputs: List(_), Nil
val second: List[Int] => Int = {
^
second: List[Int] => Int = $$Lambda$3181/1894473818@27492c62
So what did we just do? We defined second
as a reference to a function that takes a List[Int]
argument and returns an Int
(the second value in the list).
You'll notice that the Scala compiler recognizes that this is not going to match all cases and warns you of the fact. This is a partial function, in the sense that it will fail for some arguments, but it's not an instance of scala.PartialFunction
, as we can verify as follows:
scala> second.isInstanceOf[PartialFunction[List[Int], Int]]
res0: Boolean = false
Incidentally, the type List[Int] => Int
is a shorthand for scala.Function1[List[Int], Int]
and so second
s type is an instance of that type:
scala> second.isInstanceOf[Function1[List[Int], Int]]
res1: Boolean = true
Calling this version of the function produces the results you indicate:
scala> second(List(1, 2, 3))
res1: Int = 2
scala> second(Nil)
scala.MatchError: List() (of class scala.collection.immutable.Nil$)
at .$anonfun$second$1(<console>:11)
at .$anonfun$second$1$adapted(<console>:11)
... 36 elided
The problem is that if we just have some list value, l
, and don't know what is in that list, we don't know whether we'll get an exception if we pass it to the function referenced by second
. Now, we could put the call in a try
block and catch
any exception, but that's verbose and not good functional programming style. Ideally, we'd like to know whether we can call the function first to avoid an exception. Unfortunately, there's no way to tell from a Function1
instance:
scala> second.isDefinedAt(Nil)
<console>:13: error: value isDefinedAt is not a member of List[Int] => Int
second.isDefinedAt(Nil)
^
What we need is to declare second
to have the type PartialFunction[List[Int], Int]
as follows:
scala> val second: PartialFunction[List[Int], Int] = {
| case x :: y :: _ => y
| }
second: PartialFunction[List[Int],Int] = <function1>
(BTW, note that you have a typo in your question for this code - the above is how this should be defined.)
Now we do not have any warnings! We've told the compiler that this is a PartialFunction
instance, so the compiler knows that its undefined for some arguments, so warnings are superfluous. We can now verify that fact:
scala> second.isInstanceOf[PartialFunction[List[Int], Int]]
res6: Boolean = true
We can now also verify whether it's defined for particular values:
scala> second.isDefinedAt(Nil)
res7: Boolean = false
scala> second.isDefinedAt(List(1, 2))
res9: Boolean = true
and so on. (The Scala compiler, as described in the book, is able to implement this magical isDefinedAt
function for us.)
So, does that mean we should now write code like this:
def getSecondValue(l: List[Int]): Option[Int] = {
// Check if second is defined for this argument. If so, call it and wrap in Some.
if(second.isDefinedAt(l)) Some(second(l))
// Otherwise, we do not have a second value.
else None
}
Well, that's a little verbose too. Fortunately, once second
is a PartialFunction
instance, we can rewrite the above as:
def getSecondValue(l: List[Int]): Option[Int] = second.lift(l)
The lift
method turns a partial function into a complete function that returns a defined value for every argument: if the argument to second
is defined, then we get a Some(value)
; otherwise, we get None
.
You'll find the concept of partial functions, and PartialFunction
, more useful as you become more familiar with functional programming. If you don't get it right now, don't worry; all will become clear.