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:
Explains the motivation and background of partial functions.
Shows the long form and shorthand commonly used to write partial functions.
Describes the relationship between Function1 and PartialFunction.
Provides simple working code examples of partial functions.
Shows how multiple values can be handled by partial functions.
Illustrates how partial functions can be chained/composed together.
Brings out the big gun: why the collect combinator is so powerful when used with partial functions.
Shows how case sequences are actually partial functions, and points out that you have already seen these without knowing what they were.
Revisits the parametric ’with pattern’ that we have seen before, this time using partial functions.
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.
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.
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.
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.
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:
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:
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:
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:
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
}
}
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.
/** @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:
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:
At least one case has a guard or matches on a type other than Any.
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:
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.
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.
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)
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:
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