Mike Slinn

Partial Functions

— Draft —

Published 2013-02-12. Last modified 2015-09-23.
Time to read: 11 minutes.

A partial function is a way of defining anonymous functions that gives the benefits of pattern matching like casting-done-right, guards, and destructuring, while handling invalid values elegantly.

This lecture:

  1. Explains the motivation and background of partial functions.
  2. Shows the long form and shorthand commonly used to write partial functions.
  3. Describes the relationship between Function1 and PartialFunction.
  4. Provides simple working code examples of partial functions.
  5. Shows how multiple values can be handled by partial functions.
  6. Illustrates how partial functions can be chained/composed together.
  7. Brings out the big gun: why the collect combinator is so powerful when used with partial functions.
  8. Shows how case sequences are actually partial functions, and points out that you have already seen these without knowing what they were.
  9. Revisits the parametric ’with pattern’ that we have seen before, this time using partial functions.
  10. Concludes with a challenging exercise.

The sample code for this lecture can be found in courseNotes/src/main/scala/PartialFun.scala.

Motivation

Partial functions have the following usages:

  • Processing a collection such that output is only provided for those items in the collection that are matched by the partial function.
  • Chaining partial functions using PartialFunction.orElse or PartialFunction.applyOrElse, so that an input will be tried on each of a sequence of partial functions until one of them is defined for that input.
  • As case sequences, which allows you to write partial functions as a multi-way branch instead of having to chain them together.

Scala’s try / catch construct uses partial functions, as we shall see. Scala’s Future makes extensive use of partial functions. We will discuss them in several lectures starting with Futures & Promises.

Review

The shorthand for Function1[A, B] is (A) => B, and Scaladoc uses this shorthand

We learned in the Functions are First Class lecture of the Introduction to Scala course that a Function1 is parametric in two types: one type parameter for the input and one type parameter for the output. Here we have aFunction1 called f1 that merely transforms an Int into the String representation of the Int:

Scala REPL
scala> val f1 = (a: Int) => a.toString
f1: Int => String = <function1> 

You could also write shorthand:

Scala REPL
scala> val f2 = (_: Int).toString
f2: Int => String = <function1> 

You can invoke either Function1 as you would expect:

Scala REPL
scala> f1(3 * 10)
res0: String = 30 

That same lecture introduced the shorthand for Function1 as (A) => B, and because the parentheses are optional, Function1 can also be expressed as A => B. We learned earlier that Scaladoc uses this shorthand instead of writing Function1.

PartialFunction is a Subtype of Function1

The inheritance diagram for PartialFunction shows that PartialFunction is a subtype of Function1.

This means everything you know about Function1 applies equally well to PartialFunction. You might think that the name PartialFunction suggests something different, that PartialFunction is somehow less than a Function1. This is not the case; PartialFunction is actually a specialized Function1.

Partial functions have no relationship to partially applied functions, which we will learn about in the Partially Applied Functions lecture later in this course. Partial functions are used quite a lot with Actors, which we discuss in the Introduction to Actors lecture later in this course.

Definition

A partial function is a Function1 which can be defined only for a subset of the possible values of its parameters. For example, a piecewise-continuous function could be represented by a partial function. Discontinuous functions can also be represented by partial functions. Venn diagrams and graphs could be used to graphically depict partial functions. Of course, a continuous function could also be represented by a partial function because a PartialFunction is a subtype of Function1.

So that your code does not encounter exceptions needlessly, partial functions allow the validity of input values to be tested without actually invoking the partial function. If a partial function is passed input for which it is not defined, an Exception is often thrown. It’s important to note, according to the Scala documentation, that “it is the responsibility of the caller to call isDefinedAt before calling apply, because if isDefinedAt is false, it is not guaranteed apply will throw an exception to indicate an error condition. If an exception is not thrown, evaluation may result in an arbitrary value.”

PartialFunction instances are always parametric in two types, just like Function1: one type for the input parameter and one type for the output parameter. For example, a PartialFunction[X, Y] accepts an instance of X and returns an instance of Y.

Example

To set up for an example, let’s write some code that shows keys defined in the and environment variables.

scala> System.getProperties.keySet
res1: java.util.Set[Object] = [java.runtime.name, sun.boot.library.path, java.vm.version, java.vm.vendor, java.vendor.url, path.separator, java.vm.name, file.encoding.pkg, user.country, sun.java.launcher, sun.os.patch.level, java.vm.specification.name, user.dir, java.runtime.version, java.awt.graphicsenv, java.endorsed.dirs, os.arch, java.io.tmpdir, line.separator, java.vm.specification.vendor, os.name, sun.jnu.encoding, java.library.path, java.specification.name, java.class.version, sun.management.compiler, os.version, user.home, user.timezone, scala.home, java.awt.printerjob, file.encoding, java.specification.version, scala.usejavacp, java.class.path, user.name, java.vm.specification.version, sun.java.command, java.home, sun.arch.data.model, use...

scala> System.getenv.keySet
res2: java.util.Set[String] = [JAVA_HOME, PWD, DBUS_SESSION_BUS_ADDRESS, DISPLAY, USER, LS_COLORS]

scala> val home = Option(System.getenv("JAVA_HOME"))
home: Option[String] = Some(/usr/lib/jvm/java-8-oracle)

We learned about wrapping methods that might return null within Option as part of the Option, Some and None lecture of the Introduction to Scala course.

Now we can define a partial function that returns the value of an environment variable if defined:

val env = new PartialFunction[String, String] {
  private def value(name: String) = Option(System.getenv(name))

  def apply(name: String): String  = value(name).get

  def isDefinedAt(name: String): Boolean  = value(name).isDefined
}

Note that the definition of env above is parametric – the first parametric type (String) pertains to the input, and the second parametric type (also String) pertains to the returned value.

Now let’s use the env partial function:

scala> env.isDefinedAt("JAVA_HOME")
res3: Boolean = true

scala> env("JAVA_HOME")
res4: String = /usr/lib/jvm/java-8-oracle

scala> env.isDefinedAt("x") 
res5: Boolean = false

scala> env("x")
java.util.NoSuchElementException: None.get

Shorthand Notation

There is a shorthand notation (also known as syntactic sugar) for Scala partial functions, which is simply to use one or more case statements within curly braces. Let’s rewrite the partial function introduced above using this syntactic sugar:

scala> val env: PartialFunction[String, String] = {
     |   case name: String if Option(System.getenv(name)).isDefined => System.getenv(name)
     | }
env: PartialFunction[String,String] = <function1>

When writing using shorthand notation, the compiler automatically generates the PartialFunction.apply method from the case statement(s) you wrote, and the guard expression(s) you wrote is/are used to automatically generate the isDefinedAt method. Note that this version of the partial function might issue a Match Error Exception, rather than a No Such Element Exception when called with an argument it is not defined for.

The type declaration PartialFunction[String, String] is required because the enclosing context does not otherwise provide information about the acceptable return type. Here is the error message you encounter when the compiler can’t figure out the return type of a partial function:

scala> val env = {
   case name: String if Option(System.getenv(name)).isDefined => System.getenv(name)
 }
<console>:8: error: missing parameter type for expanded function
   The argument types of an anonymous function must be fully known. (SLS 8.5)
   Expected type was: ?

Multiple Inputs

If several values must be passed into a partial function, tuples, case classes or even regular classes must be used to package those values together. If you define a regular class to hold the values, be sure to define unapply in the companion object; case classes do this for you automatically, and so do tuples, because tuples are actually case classes as we learned in the Tuples Part 1 lecture of the Introduction to Scala course. You need to define unapply because partial functions are often used for pattern matching, and Scala invokes unapply during the matching process.

Here is an example of a partial function that accepts a Tuple2[String, Int] and returns a Boolean, written using shorthand notation:

scala> val checkStringLength: PartialFunction[(String, Int), Boolean] = {
  case (string: String, length: Int) if string.toLowerCase==string => string.length==length
}
checkStringLength: PartialFunction[(String, Int),Boolean] = <function1>

This partial function is not defined if the input String has any upper-case characters. The case body compares the length of the String to the provided Int, and returns true if they are the same, or false otherwise.

We can invoke this partial function’s isDefinedAt and default apply methods as follows. Notice the double parentheses – the outermost are to enclose the parameters, and the innermost indicate that these values form a tuple.

scala> checkStringLength.isDefinedAt(("asdf", 4))
res6: Boolean = true

scala> checkStringLength(("asdf", 4))
res7: Boolean = true

The Scala compiler automatically converts parameters into a tuple if required, without any warning, provided that the receiving method signature requires a single parameter that is a tuple. This means that you can also write:

scala> checkStringLength("asdf", 4)
res8: Boolean = true

scala> checkStringLength("asdf", 43)
res9: Boolean = false

We know that passing in a string with any capital letters will cause the partial function to be undefined:

scala> checkStringLength.isDefinedAt(("ASDF", 4)
res10: Boolean = false

Now let’s call the apply method for a value that is not defined by the partial function, just so we can see the error message:

scala> checkStringLength("ASDF", 4)
scala.MatchError: (ASDF,43) (of class scala.Tuple2)
  at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:248)
  at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:246)
  at $anonfun$1.applyOrElse(<console>:7)
  at $anonfun$1.applyOrElse(<console>:7)
  at scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:36)
  ... 33 elided

Composing / Chaining Partial Functions

Let’s define a type alias, which we will call PfAnyToUnit.

scala> type PfAnyToUnit = PartialFunction[Any, Unit]
defined type alias PfAnyToUnit

As you can see, this is an alias for a partial function that accepts an instance of Any and returns Unit. Obviously, this type is only useful for side effects.

Now lets define three partial functions of type PfAnyToUnit:

scala> val int: PfAnyToUnit = { case x: Int => println("Int found") }
int: PfAnyToUnit = <function1>

scala> val double: PfAnyToUnit = { case x: Double => println("Double found") }
double: PartialFunction[Any,Unit] = < function1>

scala> val any: PfAnyToUnit = { case x => println(s"Something else found ($x)") }
any: PartialFunction[Any,Unit] = < function1>

You can define a new partial function from a chain of PartialFunctions; another way to say this is that a new partial function can be composed from a series of partial functions. When we apply the new partial function, the case statement in the first PartialFunction in the chain that isDefined for the argument will be invoked, and the remainder of case statements in the chain will be skipped.

scala> val chainedPF = int orElse double orElse any
chainedPF: PartialFunction[Any,Unit] = <function1>

scala> chainedPF(1)
Int found

Here is an equivalent partial function:

scala> val chainedPF2: PfAnyToUnit = {
  case _: Int => println("Int found")

  case _: Double => println("Double found")

  case x => println(s"Something else found ($x)")
}
chainedPF2: PartialFunction[Any,Unit] = <function1>

scala> chainedPF2(1.0)
Double found

We can also create an anonymous partial function, like this. I’ll invoke it twice; this actually creates two anonymous classes, one for each expression:

scala> (int orElse double orElse any)(1.0)
Double found

scala> (int orElse double orElse any)(true)
Something else found

collect

Collect is a combinator that obtains all defined results of applying a partial function to a collection. In other words, collect is like map, but it applies a partial function to each element of a list, instead of applying a Function1. If the partial function is not defined for an element input, that element is ignored. You could accomplish the same thing using for comprehensions or map/flatMap and filter, or even map with match, but partial functions are clearer and more efficient. To be explicit, given a partial function pf, list.collect(pf) is like list.filter(pf.isDefinedAt).map(pf).

We’ll use the env partial function in conjunction with the collect combinator to search through a list of environment variables that point to multiple JVMs. The elements of the list are ordered from most desirable to least desirable. Only JVMs that are installed have values; the environment variables that refer to JVMs which are not installed have no value. Our task is to write an expression that selects the most desirable JVM. Here is we can do it using collect:

scala> val javaHomes = List("JAVA_HOME_8", "JAVA_HOME_7", "JAVA_HOME_6", "JAVA_HOME_5", "JAVA_HOME")
javaHomes: List[String] = List(JAVA_HOME_8, JAVA_HOME_7, JAVA_HOME_6, JAVA_HOME_5, JAVA_HOME)

scala> javaHomes.collect(env)
res8: List[String] = List(/usr/lib/jvm/java-7-oracle)

collect builds a new collection by applying a partial function to all elements of this list on which the function is defined. The partial function filters and maps the list. Collect returns the new list resulting from applying the partial function to each element on which it is defined. The order of the elements is preserved. You may prefer to write this using infix notation, like this:

scala> javaHomes collect env
res8: List[String] = List(/usr/lib/jvm/java-7-oracle)

Exercise – Square Root of Positive Number

Write a partial function that computes the square root of a positive number; call it squareRoot. It should be undefined for negative numbers.

When you invoke the partial function you should get these results:

scala> squareRoot.isDefinedAt(-1)  
res10: Boolean = false

scala> squareRoot(3)  
res11: Double = 1.7320508075688772

Solution

val squareRoot: PartialFunction[Double, Double] = {
   case d if d >= 0 => math.sqrt(d)
}

You can run this program by typing:

$ sbt "runMain solutions.PartialFunSoln"

Composing and Collecting Examples

Example 1

Now let’s apply the squareRoot partial function to a collection. Notice that the resulting collection only contains computed values corresponding to inputs for which the partial function is defined. Input values for which the partial function is not defined are ignored and are not reflected in the output:

scala> List(0.5, -0.2, 4).collect(squareRoot) 
res12: List[Double] = List(0.7071067811865476, 2.0)

You can also write:

scala> List(0.5, -0.2, 4) collect squareRoot
res12: List[Double] = List(0.7071067811865476, 2.0)

Example 2

As I mentioned earlier, list.collect(pf) is like list.filter(pf.isDefinedAt).map(pf), so collect selects items in a collection that match a pattern and returns a transformation of the collection.

scala> val sample = 1 to 10
sample: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

scala> val isEven: PartialFunction[Int, String] = { case x if x % 2 == 0 => x + " is even" }
isEven: PartialFunction[Int,String] = <function1>

scala> val evenNumbers = sample collect isEven
evenNumbers: scala.collection.immutable.IndexedSeq[String] = Vector(2 is even, 4 is even, 6 is even, 8 is even, 10 is even)

scala> val isOdd: PartialFunction[Int, String] = { case x if x % 2 == 1 => x + " is odd" }
isOdd: PartialFunction[Int,String] = <function1>

scala> val oddNumbers = sample collect isOdd
oddNumbers: scala.collection.immutable.IndexedSeq[String] = Vector(1 is odd, 3 is odd, 5 is odd, 7 is odd, 9 is odd)

The orElse method allows another partial function to be chained (composed) to handle input outside the declared domain of a partial function. We could have used map instead of collect, because the partial functions cover the entire set of Int.

scala> val numbers = sample collect (isEven orElse isOdd)
numbers: scala.collection.immutable.IndexedSeq[String] = Vector(1 is odd, 2 is even, 3 is odd, 4 is even, 5 is odd, 6 is even, 7 is odd, 8 is even, 9 is odd, 10 is even)

scala> val numbers2 = sample map (isEven orElse isOdd)
numbers2: scala.collection.immutable.IndexedSeq[String] = Vector(1 is odd, 2 is even, 3 is odd, 4 is even, 5 is odd, 6 is even, 7 is odd, 8 is even, 9 is odd, 10 is even)

Exercise - AndThenTry

As we learned in the Try and try/catch/finally lecture of the Introduction to Scala course, you can maintain the integrity of an application by wrapping computations that might throw an Exception in a Try. At the time I mentioned that Try did not provide the ability to chain success/failure clauses with andThen.

Your task for this exercise is to enrich Try with an andThen combinator. The andThen combinator just passes the original result along unchanged, and performs whatever side effect code is passed in. We discussed class enrichment in the Implicit Classes lecture.

Here is how andThen could be used:

val x: Try[Int] = Try {
  2 / 0
} andThen {
  case Failure(ex) => println(s"Logging ${ex.getMessage}")
} andThen {
  case Success(value) => println(s"Success: got $value")
  case Failure(ex) => println(s"This just shows that any failure is provided to each chained andThen clause ${ex.getMessage}")
}

As you can see, andThen accepts a partial function. Here is the outline of the enrichment class:

import scala.util.{Failure, Success, Try}

implicit class RichTry[A](theTry: Try[A]) {
  def andThen(pf: PartialFunction[Try[A], Unit]): Try[A] = {
    // write your solution here
  }
}

Solution

The solution is provided as solutions.AndThenTry.

import scala.util.{Failure, Success, Try}

implicit class RichTry[A](theTry: Try[A]) {
  def andThen(pf: PartialFunction[Try[A], Unit]): Try[A] = {
    if (pf.isDefinedAt(theTry)) pf(theTry)
    theTry
  }
}

You can run this solution by typing:

$ sbt "runMain solutions.AndThenTry"

This code is provided as part of the scalacourses-utils project on GitHub.

Case Sequences Are Partial Functions

A sequence of case statements is implemented as a subclass of Function1. In other words, whenever a Function1 is accepted, you can supply one or more case statements. You can use this knowledge to write more succinct code.

For example, given a list of Option[Int]:

val list = List(Some(1), None, Some(3))

Here is some verbose code that returns the contents of the Some instances and ignores all the Nones:

list collect { item =>
  item match {
    case Some(x) => x
  }
}

Instead, write this more succinct code, which uses a case sequence:

list collect { case Some(x) => x }

try / catch uses Partial Functions

By now you have seen this type of code several times without realizing that the catch block is a partial function:

def doSomething[T](data: T)(operation: T => T) = try {
   operation(data)
} catch {
  case ioe: java.io.IOException => println(ioe.getMessage)
  case   e: Exception => println(e)
}

More Than Two Cases to Handle Try

Sometimes when you only see something written one way over and over you don’t realize that other variants are possible. Consider Try: it can return Success or Failure, so only two match clauses are possible, right? Wrong! Guards can be added to a case, like this:

def handleTry(theTry: Try[String]): Unit = theTry match {
  case Success(word) if word.toLowerCase.contains("secret") =>
    println(s"You typed the secret word!")

  case Success(value) if value.trim.nonEmpty =>
    println(s"Found an unexpected word: ’$value’\n")

  case Success(whatever) =>
    println(s"Got an empty string")

  case Failure(err) =>
    println(s"Error: ${err.getMessage}")
}

Let’s use this method:

scala> handleTry(Success("blah"))
Found an unexpected word: ’blah’

scala> handleTry(Failure(new Exception("blah")))
Error: blah

scala> handleTry(Try("I know your secret"))
You typed the secret word!

scala> handleTry(Try("  "))
Got an empty string

You can run this code by typing:

$ sbt "runMain PartialFun3"

Exercise – Trying Harder

It is often useful to initiate multiple tasks and then return a collection of results, some of which may have failures. To assist you to write this sort of code, you will need a way to manipulate the collections of successes and failures. In this exercise you will develop several methods that work on collections of Try.

Implement the following methods. Please refer to the Try and try/catch/finally lecture of the Introduction to Scala course if you need to review how Try works.

/** @return collection of all failures from the given collection of Try */
def failures[A](tries: Seq[Try[A]]): Seq[Throwable] = ???

/** @return collection of all successful values from the given collection of Try */
def successes[A](tries: Seq[Try[A]]): Seq[A] = ???

/** @return a Try of a collection from a collection of Try.
  * If one or more Exceptions are encountered, only one Exception needs to be captured. */
def sequence[A](tries: Seq[Try[A]]): Try[Seq[A]] = ???

/** @return a Tuple2 containing a collection of all failures and all the successes */
def sequenceWithFailures[A](tries: Seq[Try[A]]): (Seq[Throwable], Seq[A]) = ???

Here is an example of how the methods should work:

scala> val tries = List(Try(6/0), Try("Happiness " * 3), Failure(new Exception("Drat!")), Try(99))
tries: List[scala.util.Try[Any]] = List(Failure(java.lang.ArithmeticException: / by zero), Success(Happiness Happiness Happiness ), Failure(java.lang.Exception: Drat!), Success(99))

scala> failures(tries).map(_.getMessage).mkString("; ")
res0: String = / by zero; Drat!

scala> successes(tries)
res1: Seq[Any] = List("Happiness Happiness Happiness ", 99)

scala> sequence(tries)
res0: scala.util.Try[Seq[Any]] = Failure(java.lang.ArithmeticException: / by zero)

scala> val (s, f) = sequenceWithFailures(tries)
s: Seq[Throwable] = List(java.lang.ArithmeticException: / by zero, java.lang.Exception: Drat!)
f: Seq[Any] = List("Happiness Happiness Happiness ", 99)

Solution

The solution is pleasingly succinct:

def failures[A](tries: Seq[Try[A]]): Seq[Throwable] = tries.collect { case Failure(t) => t }

def successes[A](tries: Seq[Try[A]]): Seq[A] = tries.collect { case Success(t) => t }

def sequence[A](tries: Seq[Try[A]]): Try[Seq[A]] = Try(tries.map(_.get))

def sequenceWithFailures[A](tries: Seq[Try[A]]): (Seq[Throwable], Seq[A]) = (failures(tries), successes(tries))

You can run this by typing:

$ sbt "runMain solutions.Trying"

Output is as shown above.

PartialFunction and Function1 are Interchangeable

When writing a PartialFunction using the shorthand, you know that it is not just a Function1 if one or more of the following is true:

  1. At least one case has a guard or matches on a type other than Any.
  2. There are at least two cases (a case sequence).
    {
      case x: Int => x.toString
      case y: String => y
    }

Because the Scala compiler converts Function1 instances into PartialFunction instances automatically, you can write a Function1 whenever you need a PartialFunction. Because a PartialFunction is a subtype of Function1, you can write a PartialFunction whenever you need a Function1.

Lifting a PartialFunction into a Function1 That Returns an Option

Instead of a PartialFunction, which is defined for a set or range of values, and throws a MatchException for other inputs, you might find yourself in a circumstance where you prefer a Function1 that returns an Option, so None is returned when the function is invoked with input outside the defined range of values. Scala’s PartialFunction trait has a method named lift which converts a PartialFunction[A, B] into a Function1[A, Option[B]]. Let’s try it out:

scala> val squareRoot: PartialFunction[Double, Double] = {
   case d if d >= 0 => math.sqrt(d)
 }
squareRoot: PartialFunction[Double,Double] = <function1>

scala> squareRoot(3)
res38: Double = 1.7320508075688772

scala> squareRoot.isDefinedAt(-1)
res39: Boolean = false

scala> squareRoot(-1)
scala.MatchError: -1.0 (of class java.lang.Double)
  at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:248)

scala> val squareRootFn = squareRoot.lift
squareRootFn: Double => Option[Double] = <function1>

scala> squareRootFn(3)
res41: Option[Double] = Some(1.7320508075688772)

scala> squareRootFn(-1)
res42: Option[Double] = None

unlift

Now consider the reverse case, where you have a Function1 and what you really want is a PartialFunction. Scala’s Function object has a method named unlift which converts a Function1[A, Option[B]] into a PartialFunction[A, B]. This is particularly useful when using many of the methods in Scala’s collections library that return Options, but you really want a PartialFunction. Here’s an example:

scala> val testMap = Map(1 -> "Foo", 3 -> "Bar", 5 -> "Baz")
testMap: scala.collection.immutable.Map[Int,String] = Map(1 -> Foo, 3 -> Bar, 5 -> Baz)

scala> testMap.get(3)
res43: Option[String] = Some(Bar)

scala> testMap.get(4)
res44: Option[String] = None

scala> val getPF = Function.unlift( (i: Int)  => testMap.get(i))
getPF: PartialFunction[Int,String] = <function1>

scala> getPF.isDefinedAt(3)
res45: Boolean = true

scala> getPF.isDefinedAt(4)
res46: Boolean = false

scala> getPF(3)
res47: String = Bar

scala> getPF(4)
scala.MatchError: 4 (of class java.lang.Integer)
  at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:248)

Because lift is defined on the PartialFunction trait and unlift is defined on the Function object, they must be invoked differently.

Parametric ’With’ Pattern Revisited

We first explored the With pattern in the More Fun With Functions lecture of the Introduction to Scala course, extended it in the ’With’ Pattern Using Implicits section of the Implicit Values lecture of this course, and further enhanced it in the ’With’ Pattern Revisited section of the Parametric Types lecture.

Case sequences are a type of partial function, and a PartialFunction is a specialized Function1, so we are able to pass in a case sequence for the body of the With pattern.

def withT[T](t: T)(operation: T => Unit): Unit = { operation(t) }

case class Blarg(i: Int, s: String)

def handle(blarg: Blarg): Unit = withT(blarg) {
  case Blarg(0, s) => println("i is 0")
  case Blarg(i, "triple") => println("s is triple")
  case whatever => println(whatever)
}

Let’s try out this code:

scala> handle(Blarg(1, "blarg"))
Blarg(1,blarg)

scala> handle(Blarg(1, "triple"))
s is triple

scala> handle(Blarg(0, "triple"))
i is 0

We will further enhance the With pattern in the Structural Types With Parametrics section of the Structural Types lecture by using duck typing to allow us to manage resources effectively.

Implicits and Case Sequences

Unfortunately, there is no way to use implicits with case sequences, instead, you must resort to the more verbose match construct.

def showBlarg(msg: String)(implicit blarg: Blarg): Unit =
  println(s"$msg\nblarg.i=${blarg.i}; blarg.s=${blarg.s}")

withT(Blarg(1, "blarg ")) { implicit blarg =>
  blarg match {
    case Blarg(0, s) => showBlarg("Matched blarg on i==0")
    case Blarg(i, "triple") => showBlarg("""Matched blarg on s=triple""")
    case whatever => showBlarg("Catchall case")
  }
}

Output is:

Catchall case
blarg.i=1; blarg.s=blarg

Exercise - Body Mass Index

This exercise should take at least an hour. The body mass index (BMI), or Quetelet index, is a measure for human body shape based on an individual’s weight and height. It was devised between 1830 and 1850 by Adolphe Quetelet of Belgium. A simplistic body mass index definition is the individual’s body mass divided by the square of their height. Better computations of BMI involve statistical lookups.

This Github project consists of a single PHP web page that accepts some variables and computes BMI: https://github.com/pimteam/BMI-Calculator. As you can see from examining the source code, you can specify English units or metric. The script is live at https://calendarscripts.info/overweight-calculator.html

Your task is to define a partial function that accepts three values: units, age, height and weight. If units is "English", then you will need to obtain the values as a string, and parse out feet and inches. Because these calculations assume an adult, the value of the age parameter must be 18 or more in order for the partial function to be defined; the computation also becomes meaningless for people over 100 years of age. Write a console app that performs a POST to the web page and parse the result, then displays the BMI on the console.

Hint:

  1. Pass a tuple into a partial function like this:
    scala> val myPartialFunction: PartialFunction[Tuple3[Int, Int, Int], String] = { case (a, b, c) => s"a=$a; b=$b; c=$c" }
    myPartialFunction: PartialFunction[(Int, Int, Int),String] = <function1>
    
    scala> Some((1, 2, 3)).collect(myPartialFunction)
    res1: Option[String] = Some(a=1; b=2; c=3)
    If you find that hard to read, consider this equivalent code:
    scala> type PF3 = PartialFunction[Tuple3[Int, Int, Int], String]
    defined type alias pf3
    
    scala> val myPartialFunction: PF3 = { case (a, b, c) => s"a=$a; b=$b; c=$c" }
    myPartialFunction: PF3 = <function1>
    
    scala> Some((1, 2, 3)) collect myPartialFunction
    res2: Option[String] = Some(a=1; b=2; c=3)
    
  2. If you want to operate on a single variable, instead of a collection, you can use the partial function’s isDefinedAt method, then call the method, like this:
    scala> val tuple = ((1, 2, 3))
    tuple: (Int, Int, Int) = (1,2,3)
    
    scala> myPartialFunction.isDefinedAt(tuple)
    res3: Boolean = true
    
    scala> myPartialFunction(tuple)
    res4: String = a=1; b=2; c=3
    

Once your console application prints the BMI for a single person, modify your program to read in data for several people from a file.

Solution

This solution is provided in the solutions package of the courseNotes project. This is BMISolution.scala:

package solutions

object BMISolution extends App {
  import solutions.BMICalc._

  case class Person(name: String, gender: Gender, age: Int, height: Meters, weight: Kg)  {
    def formattedStr = f"$name%-25s is $gender%6s, age: $age%3d, height: $height%4.2f Meters, weight: $weight%5.1f Kilos"
    def formattedStrEng = f"$name%-25s is $gender%6s, age: $age%3d, height: ${metersToFtIn(height)._1}%1.0f Ft, ${metersToFtIn(height)._2}%4.1f In, weight: ${kgToLbs(weight)}%6.1f Lbs"
  }

  object Person {
    def makePersonEng(name: String, gender: Gender, age: Int, height: (Ft, In), weight: Lbs): Person =
      Person(name, gender, age, ftInToMeters(height) , lbsToKg(weight))
  }

  val fatAlbert = Person("Fat Albert", Gender.Male, 25, 1.8, 110)
  val oldGuy = Person("Old Guy", Gender.Male, 60, 1.82, 88 )
  val bigBernie = Person("Big Bernie", Gender.Male, 25, 2.6, 400)
  val maxDude = Person("Max Dude",Gender.Male, 20, .5, 560)
  val chandraDangi = Person("Chandra Bahadur Dangi", Gender.Male, 76, .55, 15)
  val khalidShaar = Person("Khalid Bin Mohsen Shaar", Gender.Male, 25, 1.73, 610 )
  val jonMinnoch = Person("Jon Brower Minnoch", Gender.Male, 42, 1.85, 635)
  val sultanK├╢sen = Person("Sultan K├╢sen", Gender.Male, 35, 2.51, 137)
  val manuelUribe = Person("Manuel Uribe", Gender.Male, 48, 1.96, 597)
  val carolYager = Person("Carol Ann Yager", Gender.Female, 48, 1.7, 544)
  val shortHeavy = Person("Short Heavy",Gender.Male, 20, .5, 560)
  val tallLight = Person("Tall Light", Gender.Female, 60, 2.6, 15)
  val joeFit = Person.makePersonEng("Joe Fit", Gender.Male, 30,(5, 11), 150)
  val atheleticJane = Person.makePersonEng("Athletic Jane", Gender.Female, 35,(5, 5), 105)

  val people = List(fatAlbert, oldGuy, bigBernie, maxDude, chandraDangi, khalidShaar, jonMinnoch, sultanK├╢sen, manuelUribe, carolYager, shortHeavy, tallLight, joeFit, atheleticJane)

  /*
   *  In calculating the BMI we check to see that:
   *    age is 20 or over and 100 or under
   *    weight is between 15 and 640 Kg
   *    height is between .5 meter and 2.6 meters
   */
  val getBMI: PartialFunction[Person, Double] = {
    case person if person.age >= 20 && person.age <= 100 &&
      person.height >= .5 && person.height <= 2.6 &&
      person.weight >= 15 && person.weight <= 640 => bmi(person.weight, person.height)
  }
  val getBMIO = getBMI.lift

  def getBMIDataO(person: Person): Option[(Double, BMICategory, Int)] = {
    if (getBMI.isDefinedAt(person)) {
      val bmi = getBMI(person)
      Option(bmi, BMICategory.getBMICategory(bmi), getBMIPercentile(person.gender, person.age, bmi))
    }
    else None
  }
  val getBMIData: PartialFunction[Person, (Double, BMICategory, Int)] = Function.unlift(getBMIDataO)
  def formatBMIData(bMIData: (Double, BMICategory, Int)): String = f"BMI = ${bMIData._1}%5.1f, Percentile = ${bMIData._3}%2d, ${bMIData._2.formattedStr} "

  println("====People in Metric Units====")
  people.foreach{(person) => {
  print(person.formattedStr + " ")
  println(formatBMIData(getBMIData(person)))
  }}

  println("====People in English Units====")
  people.foreach{(person) => {
    print(person.formattedStrEng + " ")
    println(formatBMIData(getBMIData(person)))
  }}
}

This is BMICalc.scala:

package solutions

object BMICalc {

  /** Define types for the Metric and English units for documentation purposes, and to keep our units straight */
  type Kg = Double
  type Meters = Double

  /*
   * These function both calculate BMI, given metric units. bmiPartial is a partial function that does some input checking.
   */
  def bmi(weight: Kg, height: Meters): Double = weight / (height * height)
  val bmiPartial: PartialFunction[(Kg, Meters), Double] = {
    case (weight, height) if weight >= 15 && weight <= 640 && height >= .5 && height <= 2.6 => weight / (height * height)
  }

  /*
   * The following  calculates the set of BMI categories, taken from https://en.wikipedia.org/wiki/Body_mass_index
   * There is some feeling that these categories are actually unrealistic.
   */
  sealed trait BMICategory {
    def msg: String
    def bMIRange: (Double, Double)
    def inRange(bmi: Double): Boolean = { bmi >= bMIRange._1 && bmi <= bMIRange._2  }
    def formattedStr = f"BMI category is $msg%20s (BMI range: ${bMIRange._1}%5.2f,${bMIRange._2}%5.2f)"
  }
  object BMICategory {
    case object VeryServerlyUnderWeight extends BMICategory {val msg = "Very severely underweight";val bMIRange = (0.0, 15.0)}
    case object ServerlyUnderWeight extends BMICategory {val msg = "Severely underweight";val bMIRange = (15.0, 16.0)}
    case object UnderWeight extends BMICategory {val msg = "Underweight";val bMIRange = (16.0, 18.5)}
    case object NormalWeight extends BMICategory {val msg = "Normal weight";val bMIRange = (18.5, 25.0)}
    case object OverWeight extends BMICategory {val msg = "Overweight";val bMIRange = (25.0, 30.0)}
    case object ModeratelyObese extends BMICategory {val msg = "Moderately Obese";val bMIRange = (30.0, 35.0)}
    case object SeverelyObese extends BMICategory {val msg = "Severely Obese";val bMIRange = (35.0, 40.0)}
    case object VerySeverelyObese extends BMICategory {val msg = "Very Severely Obese";val bMIRange = (40.0, 5000.0)}
    val bmiRanges = List(VeryServerlyUnderWeight, ServerlyUnderWeight, UnderWeight, NormalWeight, OverWeight, ModeratelyObese, SeverelyObese, VerySeverelyObese)
    val getBMICategory  = Function.unlift((x: Double) => bmiRanges.find(_.inRange(x)))
  }

  /*
   *  This is a simple gender trait to tag people as Male or Female
   */
  sealed trait Gender {
    def msg: String
    def isMale: Boolean
    def isFemale: Boolean
    override def toString = msg
  }
  object Gender {
    case object Male extends Gender {
      val msg = "Male"
      val isMale = true
      val isFemale = false
    }
    case object Female extends Gender {
      val msg = "Female"
      val isMale = false
      val isFemale = true
    }
  }

  /*
   * The following code calculates where a particular BMI sits against the North American population, as a percentile.
   * The data is taken from https://en.wikipedia.org/wiki/Body_mass_index
   * Note we have added a "99th" percentile with a very high BMI value (5000) to avoid lookups of ridiculously high BMI
   * values from failing.
   */
  val percentiles = List(5, 10, 15, 25, 50, 75, 85, 90, 95, 99 )
  val ages = List(29, 39, 49, 59, 69,79, 120)
  //Data for males
  val age29m = List(19.4,    20.7,    21.4,    22.9,    25.6,    29.9,    32.3,    33.8,    36.5, 5000) zip percentiles
  val age39m = List(21.0,    22.4,    23.3,    24.9,    28.1,    32.0,    34.1,    36.2,    40.5, 5000) zip percentiles
  val age49m = List(21.2,    22.9,    24.0,    25.4,    28.2,    31.7,    34.4,    36.1,    39.6, 5000) zip percentiles
  val age59m = List(21.5,    22.9,    23.9,    25.5,    28.2,    32.0,    34.5,    37.1,    39.9, 5000) zip percentiles
  val age69m = List(21.3,    22.7,    23.8,    25.3,    28.8,    32.5,    34.7,    37.0,    40.0, 5000) zip percentiles
  val age79m = List(21.4,    22.9,    23.8,    25.6,    28.3,    31.3,    33.5,    35.4,    37.8, 5000) zip percentiles
  val age99m = List(20.7,    21.8,    22.8,    24.4,    27.0,    29.6,    31.3,    32.7,    34.5, 5000) zip percentiles
  val maleTable = ages zip List(age29m, age39m, age49m, age59m, age69m, age79m, age99m )
  //Data for females
  val age29f = List(18.8,    19.9,    20.6,    21.7,    25.3,    31.5,    36.0,    38.0,    43.9, 5000) zip percentiles
  val age39f = List(19.4,    20.6,    21.6,    23.4,    27.2,    32.8,    36.0,    38.1,    41.6, 5000) zip percentiles
  val age49f = List(19.3,    20.6,    21.7,    23.3,    27.3,    32.4,    36.2,    38.1,    43.0, 5000) zip percentiles
  val age59f = List(19.7,    21.3,    22.1,    24.0,    28.3,    33.5,    36.4,    39.3,    41.8, 5000) zip percentiles
  val age69f = List(20.7,    21.6,    23.0,    24.8,    28.8,    33.5,    36.6,    38.5,    41.1, 5000) zip percentiles
  val age79f = List(20.1,    21.6,    22.7,    24.7,    28.6,    33.4,    36.3,    38.7,    42.1, 5000) zip percentiles
  val age99f = List(19.3,    20.7,    22.0,    23.1,    26.3,    29.7,    31.6,    32.5,    35.2, 5000) zip percentiles
  val femaleTable = ages zip List(age29f, age39f, age49f, age59f, age69f, age79f, age99f )
  /*
   * We use a find, by age, on maleTable or femaleTable, extract the list of percentiles for a given age,
   * then use a find, by BMI, and extract the percentile
   * A naive implementation using combinators is:
   *    maleTable.find(age <= _._1).get._2.find(bmi <= _._1).get._2
   * The problem is that find returns an Option... and we are not handling the cases where a None is returned by the finds.
   */
  def getBMIPercentileBad(gender: Gender, age: Int, bmi: Double): Int =
    maleTable.find(age <= _._1).get._2.find(bmi <= _._1).get._2   //Find returns an option

  /*
   *  This for comprehension essentially does the same thing as getBMIPercentileBad,
   *  but it will properly handle the case where a None is returned
   */
  def getBMIPercentileO(gender: Gender, age: Int, bmi: Double) = {
    val theTable = if (gender.isMale) maleTable else  femaleTable
    for  {
      theTableO <- theTable.find(age <= _._1)
      bmiLookUpTable <- Option(theTableO._2)
      percentile <- bmiLookUpTable.find(bmi <= _._1)
    } yield percentile._2
  }
  /*
   * Create a Partial Function version of  getBMIPercentileO. The call to tupled converts the individual function
   * parameters into a single tuple, because a PartialFunction only takes a single parameter.
   */
  val getBMIPercentile: PartialFunction[(Gender, Int, Double), Int] = Function.unlift (Function.tupled(getBMIPercentileO))

  /*
   * Conversion functions from English to Metric units, so English Units can be converted to be used with the
   * bmi function
   */
  type Lbs = Double
  type Ft = Double
  type In = Double
  val LbsToKg = 2.2046
  def lbsToKg(lbs: Lbs): Kg = lbs / LbsToKg
  val lbsToKgPartial: PartialFunction[Lbs, Kg] = {
    case lbs if lbs > 0 => lbsToKg(lbs)
  }
  def kgToLbs(kg: Kg): Lbs = kg * LbsToKg

  val MetersToInches = 39.370
  val MetersToFt =  3.2808
  def ftInToMeters(height: (Ft, In)): Meters = (height._1 / MetersToFt) + (height._2 / MetersToInches)
  val ftInToMetersPartial: PartialFunction[(Ft, In), Meters] = {
    case (ft, in) if ft > 0 && in >= 0 && in < 12 => ftInToMeters(ft, in)
  }
  def metersToFtIn(meters: Meters): (Ft, In) = {
    val grossIn = meters *  MetersToInches
    val ft = (grossIn / 12).floor
    (ft, grossIn - (ft * 12))
  }

}

You can run this solution by typing:

$ sbt "runMain solutions.BMISolution"
[info] Loading global plugins from /home/mslinn/.sbt/0.13/plugins
[info] Loading project definition from /var/work/course_scala_intermediate_code/courseNotes/project
[info] Set current project to IntermediateScalaCourse (in build file:/var/work/course_scala_intermediate_code/courseNotes/)
[info] Running solutions.BMISolution
====People in Metric Units====
Fat Albert                is   Male, age:  25, height: 1.80 Meters, weight: 110.0 Kilos BMI =  34.0, Percentile = 95, BMI category is     Moderately Obese (BMI range: 30.00,35.00)
Old Guy                   is   Male, age:  60, height: 1.82 Meters, weight:  88.0 Kilos BMI =  26.6, Percentile = 50, BMI category is           Overweight (BMI range: 25.00,30.00)
Big Bernie                is   Male, age:  25, height: 2.60 Meters, weight: 400.0 Kilos BMI =  59.2, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Max Dude                  is   Male, age:  20, height: 0.50 Meters, weight: 560.0 Kilos BMI = 2240.0, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Chandra Bahadur Dangi     is   Male, age:  76, height: 0.55 Meters, weight:  15.0 Kilos BMI =  49.6, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Khalid Bin Mohsen Shaar   is   Male, age:  25, height: 1.73 Meters, weight: 610.0 Kilos BMI = 203.8, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Jon Brower Minnoch        is   Male, age:  42, height: 1.85 Meters, weight: 635.0 Kilos BMI = 185.5, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Sultan K├╢sen              is   Male, age:  35, height: 2.51 Meters, weight: 137.0 Kilos BMI =  21.7, Percentile = 10, BMI category is        Normal weight (BMI range: 18.50,25.00)
Manuel Uribe              is   Male, age:  48, height: 1.96 Meters, weight: 597.0 Kilos BMI = 155.4, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Carol Ann Yager           is Female, age:  48, height: 1.70 Meters, weight: 544.0 Kilos BMI = 188.2, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Short Heavy               is   Male, age:  20, height: 0.50 Meters, weight: 560.0 Kilos BMI = 2240.0, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Tall Light                is Female, age:  60, height: 2.60 Meters, weight:  15.0 Kilos BMI =   2.2, Percentile =  5, BMI category is Very severely underweight (BMI range:  0.00,15.00)
Joe Fit                   is   Male, age:  30, height: 1.80 Meters, weight:  68.0 Kilos BMI =  20.9, Percentile =  5, BMI category is        Normal weight (BMI range: 18.50,25.00)
Athletic Jane             is Female, age:  35, height: 1.65 Meters, weight:  47.6 Kilos BMI =  17.5, Percentile =  5, BMI category is          Underweight (BMI range: 16.00,18.50)
====People in English Units====
Fat Albert                is   Male, age:  25, height: 5 Ft, 10.9 In, weight:  242.5 Lbs BMI =  34.0, Percentile = 95, BMI category is     Moderately Obese (BMI range: 30.00,35.00)
Old Guy                   is   Male, age:  60, height: 5 Ft, 11.7 In, weight:  194.0 Lbs BMI =  26.6, Percentile = 50, BMI category is           Overweight (BMI range: 25.00,30.00)
Big Bernie                is   Male, age:  25, height: 8 Ft,  6.4 In, weight:  881.8 Lbs BMI =  59.2, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Max Dude                  is   Male, age:  20, height: 1 Ft,  7.7 In, weight: 1234.6 Lbs BMI = 2240.0, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Chandra Bahadur Dangi     is   Male, age:  76, height: 1 Ft,  9.7 In, weight:   33.1 Lbs BMI =  49.6, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Khalid Bin Mohsen Shaar   is   Male, age:  25, height: 5 Ft,  8.1 In, weight: 1344.8 Lbs BMI = 203.8, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Jon Brower Minnoch        is   Male, age:  42, height: 6 Ft,  0.8 In, weight: 1399.9 Lbs BMI = 185.5, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Sultan K├╢sen              is   Male, age:  35, height: 8 Ft,  2.8 In, weight:  302.0 Lbs BMI =  21.7, Percentile = 10, BMI category is        Normal weight (BMI range: 18.50,25.00)
Manuel Uribe              is   Male, age:  48, height: 6 Ft,  5.2 In, weight: 1316.1 Lbs BMI = 155.4, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Carol Ann Yager           is Female, age:  48, height: 5 Ft,  6.9 In, weight: 1199.3 Lbs BMI = 188.2, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Short Heavy               is   Male, age:  20, height: 1 Ft,  7.7 In, weight: 1234.6 Lbs BMI = 2240.0, Percentile = 99, BMI category is  Very Severely Obese (BMI range: 40.00,5000.00)
Tall Light                is Female, age:  60, height: 8 Ft,  6.4 In, weight:   33.1 Lbs BMI =   2.2, Percentile =  5, BMI category is Very severely underweight (BMI range:  0.00,15.00)
Joe Fit                   is   Male, age:  30, height: 5 Ft, 11.0 In, weight:  150.0 Lbs BMI =  20.9, Percentile =  5, BMI category is        Normal weight (BMI range: 18.50,25.00)
Athletic Jane             is Female, age:  35, height: 5 Ft,  5.0 In, weight:  105.0 Lbs BMI =  17.5, Percentile =  5, BMI category is          Underweight (BMI range: 16.00,18.50)
[success] Total time: 1 s, completed Oct 4, 2015 8:55:10 AM

* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.