-
Notifications
You must be signed in to change notification settings - Fork 11
Recap: Case classes and Partial Functions
Case classes are Scala’s preferred way to define complex data. Defining a class as a case class allows decompose it using pattern matching.
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 }
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.
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)
}
}
One nice aspect of functions being traits is that we can subclass the function type.
One example is, Map
s are functions from keys to values:
trait Map[Key, Value] extends (Key => Value) ...
Another example is Sequence
s 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]
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.