scalapartial-functions

Partial function explanation in the Odersky book


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?


Solution

  • 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 seconds 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.