Skip to content

Recap: Case classes and Partial Functions

Rohit edited this page Jan 2, 2017 · 19 revisions

Case Classes:

Case classes are Scala’s preferred way to define complex data. Defining a class as a case class allows decompose it using pattern matching.

Representing JSON in Scala

Each item in the json can be represented by a case class as below:

abstract class JSON
case class JSeq (elems: List[JSON]) extends JSON  // Array
case class JObj (bindings: Map[String, JSON]) extends JSON
case class JNum (num: Double) extends JSON
case class JStr (str: String) extends JSON
case class JBool(b: Boolean) extends JSON
case object JNull extends JSON

Thus, the json below:

{
    "firstName" : "John",
    "lastName" : "Smith",
    "address" : {
        "streetAddress" : "21 2 nd Street",
        "state" : "NY",
        "postalCode": 10021
    },
    "phoneNumbers": [
        {"type" : "home","number ": "212 555 -1234"} ,
        {"type" : "fax","number ": "646 555 -4567"}
    ]
}

Can be represented as:

val data = JObj(Map(
    "firstName" -> JStr("John"),
    "lastName" -> JStr("Smith"),
    "address" -> JObj(Map(
    "streetAddress" -> JStr("21 2nd Street"),
    "state" -> JStr("NY"),
    "postalCode" -> JNum(10021)
)),
"phoneNumbers" -> JSeq(List(
    JObj(Map(
    "type" -> JStr("home"), "number" -> JStr("212 555-1234")
)),
JObj(Map(
    "type" -> JStr("fax"), "number" -> JStr("646 555-4567")
)) )) ))

Pattern matching can be done on this i.e. a method that returns the string representation of JSON data:

def show(json: JSON): String = json match {
    case JSeq(elems) =>
    "[" + (elems map show mkString ", ") + "]"
    case JObj(bindings) =>
    val assocs = bindings map {
        case (key, value) => "\"" + key + "\": " + show(value)
    }
    "{" + (assocs mkString ", ") + "}"
    case JNum(num) => num.toString
    case JStr(str) => '\"' + str + '\"'
    case JBool(b) => b.toString
    case JNull => "null"
}

Note: Check how the map() function is called on the object called bindings so that the pattern-matching takes place on all the objects. This is how you do nested-pattern-matching.

Question: In the above function, what’s the type of the pattern matching block:

{ case (key, value) => key +:+ value }

Taken by itself, the expression is not typable i.e. it cannot be given a type i.e. it cannot be expressed as val f: some-type = { case (key, value) => key + ”: ” + value }. We need to prescribe an expected type. The type expected by map on the last slide is:

JBinding => String,

the type of functions from pairs of strings and JSON data to String. where JBinding is

type JBinding = (String, JSON)

so we have:

val f: JBinding => String = { case (key, value) => key +:+ value }

Functions are Objects

In Scala, every concrete type is the type of some class or trait. The function type is no exception. A type like

JBinding => String

is just a shorthand for

scala.Function1[JBinding, String]

where scala.Function1 is a trait and JBinding and String are its type arguments.

The Function1 Trait

How the pattern matching above works:

Here’s an outline of trait Function1:

trait Function1[-A, +R] {
    def apply(x: A): R
}

The pattern matching block

{ case (key, value) => key + ": " + value }

expands to the Function1 instance

new Function1[JBinding, String] {
    def apply(x: JBinding) = x match {
        case (key, value) => key + ": " + show(value)
    }
}

Subclassing functions

One nice aspect of functions being traits is that we can subclass the function type.

One example is, Maps are functions from keys to values:

trait Map[Key, Value] extends (Key => Value) ...

Another example is Sequences are functions from Int indices to values:

trait Seq[Elem] extends Int => Elem

That’s why for sequence (and array) indexing, we can use parenthesis as we use in functions, and write:

elems(i)
// as opposed to JAVA where we would have used: 
// elems[i]

Partial Matches

We have seen that a pattern matching block like

{ case "ping" => "pong" }

can be given type String => String.

val f: String => String = { case "ping" => "pong" }

But the function is not defined on all its domain!

f("abc") // "abc" or any other string gives a MatchError

Is there a way to find out whether the function can be applied to a given argument before running it?

Indeed there is, using Partial Functions:

val f: PartialFunction[String, String] = { case "ping" => "pong" }
f.isDefinedAt("ping") // true
f.isDefinedAt("abc") // false

So basically we can call the isDefinedAt function of the partial function to check if the partal function itself can or cannot be applied to an argument.

A partial function is a function that is valid for only a subset of values of those types you might pass in to it. For example:

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

scala> root.isDefinedAt(-1)
res0: Boolean = false

scala> root(3)
res1: Double = 1.7320508075688772

The partial function trait is defined as follows:

trait PartialFunction[-A, +R] extends Function1[-A, +R] {
    def apply(x: A): R
    def isDefinedAt(x: A): Boolean
}

If the expected type is a PartialFunction, the Scala compiler will expand

{ case "ping" => "pong" }

as follows:

new PartialFunction[String, String] {
    def apply(x: String) = x match {
        case ”ping” => ”pong”
    }
    def isDefinedAt(x: String) = x match {
        case ”ping” => true
        case _ => false
    }
}

Note: isDefinedAt guarantees that partial function gives you only applies to the outermost pattern matching block.

Eg.

val f1: PartialFunction[List[Int], String] = {
    case Nil => ”one”
    case x :: y :: rest => ”two”
}
val g: PartialFunction[List[Int], String] = {
    case Nil => ”one”
    case x :: rest =>
        rest match {
            case Nil => ”two”
        }
}

f.isDefinedAt(List(1, 2, 3)) // true
g.isDefinedAt(List(1, 2, 3)) // true - but gives an error when the function is actually executed as the inner case Nil does not match the element and there is no other case that defines the integer elements.