game, set, match





patterns all around





Rose Toomey
Novus Partners


15 July 2011 @ Scalathon

Pattern Matching

  • pattern matching
  • case classes
  • apply
  • unapply
  • case objects
  • enums

Try it out!

The sample code shown in this presentation is available at:
rktoomey/scalathon-crash-course.

You can build and run the project using simple-build-tool 0.7.

The quickest way to get started experimenting is to clone the project and run sbt console to use a Scala interpreter with a classpath that includes compiled sources and managed libs:

~ $ git clone git://github.com/rktoomey/scalathon-crash-course.git
~ $ cd scalathon-crash-course
~/scalathon-crash-course $ sbt update
~/scalathon-crash-course $ sbt console

constant patterns


Here is a function that matches an incoming String against constant values.

scala> def firstTry(x: String) = x match {
     | case "Hello" => "Hi!"
     | case "Goodbye" => "Bye!"
     | case _ => "Unsure what to say"
     | }
firstTry: (x: String)java.lang.String

scala> firstTry("Hello")
res0: java.lang.String = Hi!

scala> firstTry("Goodbye")
res1: java.lang.String = Bye!

scala> firstTry("Hey")
res2: java.lang.String = Unsure what to say

scala> firstTry("hello")
res3: java.lang.String = Unsure what to say

what just happened?

  • match is an expression in Scala: it always results in a value

    • no match? a MatchError will be thrown
  • unlike switch, cases never "fall through" - there is no concept of break

    • you are required to specify a default case - in the example it was _
    • instead of using _, we could have captured the value and assigned it some name

variable patterns


  • in the default case, all we have to do it use a name, like greeting to capture the value
  • in a case where we are matching on a constant value, we use hello @ sign to capture the input "Hello"
    scala> def secondTry(x: String) = x match {
         | case hello @ "Hello" => hello.reverse
         | case greeting => "%s right back at you".format(greeting)
         | }
    secondTry: (x: String)String

    scala> secondTry("Hey")
    res5: String = Hey right back at you

    scala> secondTry("Hello")
    res6: String = olleH

    scala> secondTry("HI")
    res7: String = HI right back at you



don't worry, this information will be much more exciting when we cover case classes!

extracting a value from an Option


This is not an idiomatic way to work with Option in Scala! But it's interesting to consider how pattern matching can distinguish and extract the String sunshine here.

scala> val x: Option[String] = Some("sunshine")
x: Option[String] = Some(sunshine)

scala> x match {
     | case Some(s) => s
     | case None => "only rain"
     | }
res5: String = sunshine



A side note: do it this way instead, everyone will thank for you it... :)

scala> x.getOrElse("only rain")
res7: String = sunshine

oh, how about List?


You might have considered that the behaviour of Option is sometimes like a monad, sometimes like a collection. Methods like isEmpty, toList, map, flatMap, filter, filterNot...

Try thinking of Some as a single entry list, and of None as an empty list.

scala> List(1, 2, 3) match {
     | case List(1, _, _) => "found it"
     | case list => "not what I wanted: %s".format(list.mkString(","))
     | }
res8: java.lang.String = found it

you can't get there from here


The Scala compiler will error out when you when your cases don't make sense!

scala> def rococo(list: List[Int]) = list match {
     | case List(1, _, _) => "one is the first element of a three-element list"
     | case l @ List(1, _*) => "one is the first element of a %d-element long list"
        .format(l.size)
     | case list => "one is not the first element of this list: %s".format(list.mkString)
     | case Nil => "empty list"
     | }
<console>:15: error: unreachable code
       case Nil => "empty list"


Why is Nil after list unreachable?

you might not get there from here


Remember MatchError? The Scala compiler will warn but not stop you if your cases don't cover all the possibilities.

scala> def rococo(list: List[Int]) = list match {
     | case List(1, _, _) => "one is the first element of a three-element list"
     | case l @ List(1, _*) => "one is the first element of a %d-element long list"
        .format(l.size)
     | }
<console>:11: warning: match is not exhaustive!
missing combination            Nil

       def rococo(list: List[Int]) = list match {
                                             ^
rococo: (list: List[Int])java.lang.String

list matching: as fiddly as you want it to be

scala> def rococo(list: List[Int]) = list match {
     | case List(1, _, _) => "one is the first element of a three-element list"
     | case l @ List(1, _*) => "one is the first element of a %d-element long list"
        .format(l.size)
     | case Nil => "empty list"
     | case list => "one is not the first element of this list: %s".format(list.mkString(", "))
     | }
rococo: (list: List[Int])java.lang.String

scala> rococo(List(1, 2, 3))
res10: java.lang.String = one is the first element of a three-element list

scala> rococo(List(1, 2, 3, 4))
res11: java.lang.String = one is the first element of a 4-element long list

scala> rococo(List(4, 5, 6))
res12: java.lang.String = one is not the first element of this list: 4, 5, 6

scala> rococo(Nil)
res13: java.lang.String = empty list

case classes

  • how is a case class different to a regular class
  • how is Product useful?

all this and more

When you declare a class with the case modifier in Scala, you get a lot of built-in functionality for free!

Case classes are Scala's way to allow matching on objects without boilerplate code, but you also get:

  • every field in the constructor is a public val
  • a product iterator
  • built-in "natural" equals, hashCode and toString methods
  • apply
  • unapply
  • copy

what does Product do?

case class Alpha(a: Double, b: Long)
case class Bravo(x: String, y: Int, z: Option[Alpha])


Scala's Product trait makes a case class behave like an ordered list of fields. This capability drives equals, hashCode, toString and more.

arity

A list has a size, a case class has a number of fields up to 21.

  • arity is zero-indexed.
  • you can use the arity to get the Nth element of the product.

product iterator

A case class has a product iterator, which gives you an ordered iteration of the fields - just like iterating through a list.

does this sound familiar?

Pair and Tuple1...Tuple22 are subclasses of Product.

demonstration of what Product does


val alpha = Alpha(a = 1.4, b = 99L)
val bravo = Bravo(x = "bravo", y = 2, z = Some(alpha))


class: 'prasinous.exercises.Bravo'
arity: 3
prefix: 'Bravo'
[0]	java.lang.String		baker
[1]	java.lang.Integer		2
[2]	scala.Some		Some(Alpha(1.4,99))

toString: Bravo(baker,2,Some(Alpha(1.4,99)))

how does the compiler show this?

target/scala_2.9.0-1/classes/prasinous/exercises$ scalap -cp . "Bravo"

package prasinous.exercises

case class Bravo(x : scala.Predef.String, y : scala.Int, z : scala.Option[prasinous.exercises.Alpha])
    extends java.lang.Object with scala.ScalaObject with scala.Product with scala.Serializable {
  val x : scala.Predef.String = { /* compiled code */ }
  val y : scala.Int = { /* compiled code */ }
  val z : scala.Option[prasinous.exercises.Alpha] = { /* compiled code */ }
  def copy(x : scala.Predef.String, y : scala.Int,
    z : scala.Option[prasinous.exercises.Alpha]) : prasinous.exercises.Bravo = { /* compiled code */ }
  override def hashCode() : scala.Int = { /* compiled code */ }
  override def toString() : scala.Predef.String = { /* compiled code */ }
  override def equals(x$1 : scala.Any) : scala.Boolean = { /* compiled code */ }
  override def productPrefix : java.lang.String = { /* compiled code */ }
  override def productArity : scala.Int = { /* compiled code */ }
  override def productElement(x$1 : scala.Int) : scala.Any = { /* compiled code */ }
  override def canEqual(x$1 : scala.Any) : scala.Boolean = { /* compiled code */ }
}

what about the companion object?

target/scala_2.9.0-1/classes/prasinous/exercises$ scalap -cp . "Bravo$"

package prasinous.exercises;

final class Bravo$ extends scala.runtime.AbstractFunction3 with scala.Serializable
  with scala.ScalaObject {

  def this(): scala.Unit;
  def apply(scala.Any, scala.Any, scala.Any): scala.Any;
  def readResolve(): scala.Any;
  def apply(java.lang.String, scala.Int, scala.Option): prasinous.exercises.Bravo;
  def unapply(prasinous.exercises.Bravo): scala.Option;
  def toString(): java.lang.String;
}
object Bravo$ {
  final val MODULE$: prasinous.exercises.Bravo$;
}

three ways to create a case class


The most idiomatic way to create a case class is just to say Alpha(3.0, 4L)

scala> import prasinous.exercises._
import prasinous.exercises._

scala> val a = Alpha(3.0, 4L)
a: prasinous.exercises.Alpha = Alpha(3.0,4)

scala> val a_* = Alpha.apply(3.0, 4L)
a_*: prasinous.exercises.Alpha = Alpha(3.0,4)

scala> val a_** = new Alpha(3.0, 4L)
a_**: prasinous.exercises.Alpha = Alpha(3.0,4)

scala> a == a_* && a == a_** && a_* == a_**
res0: Boolean = true

so what does unapply do?


The unapply method converts Alpha to an Option with a Tuple2 typed to the order of the fields in Alpha. But why Option?

scala> Alpha.unapply(a)
res1: Option[(Double, Long)] = Some((3.0,4))

but why does unapply return an Option?

Can anyone think why that might be useful?

and we circle back to pattern matching!


scala> def guess(a: Alpha) = a match {
     | case Alpha(3.0, 4L) => "Princess defeats Runarstiltskin again.  Curses!"
     | case wrongOne => "Keep guessing, you haven't got my name yet... %s".format(wrongOne)
     | }
guess: (a: prasinous.exercises.Alpha)java.lang.String

scala> guess(a)
res2: java.lang.String = Princess defeats Runarstiltskin again.  Curses!

scala> guess(Alpha(7.0, 2L))
res3: java.lang.String = Keep guessing, you haven't got it yet... Alpha(7.0,2)

copy: the good


The ease of copy with case classes puts doddering, inconvenient java.lang.Cloneable to shame:

scala> a
res18: prasinous.exercises.Alpha = Alpha(3.0,4)

scala> a.copy(a = 3.14)
res19: prasinous.exercises.Alpha = Alpha(3.14,4)

copy: the wtf

There's only one catch: for some reason that has never been explained to my satisfaction, the Scala compiler does not enforce copy on the case class. So you can depend on copy being present:

  • within the case class itself
  • on an instance of the case class



But nothing guarantees that copy will actually be present. Notice that we you can't override the copy method as you might expect.

scala> case class Foo(x: String) {
     | override def copy(x: String) = throw new Error("LOL")
     | }
<console>:11: error: method copy overrides nothing
       override def copy(x: String) = throw new Error("LOL")

where we are so far


So far, we've seen examples of:

  • pattern matching using a constant value
  • pattern matching using a case class


what else can we do with pattern matching?

  • pattern matching on type
  • guards
  • case object and enums

typed patterns

scala> def matchOnType[A <: AnyRef](a: A) = a match {
     | case s: String => "String: %s".format(s)
     | case d: java.util.Date => "Date: %s".format(d)
     | case m: Map[_, _] => "Map: %s".format(m.mkString)
     | case x => "??? %s with value %s".format(x.getClass.getName, x)
     | }
matchOnType: [A <: AnyRef](a: A)String

scala> matchOnType("moon")
res14: String = String: moon

scala> matchOnType(a)
res16: String = ??? prasinous.exercises.Alpha with value Alpha(3.0,4)

the cruelty of type erasure

scala> def isIntMap(m: Any) = m match {
     | case Map[Int, Int] => true
     | case _ => false
     | }
<console>:11: error: type Map of type Map does not take type parameters.
       case Map[Int, Int] => true



If you chose to run Scala unchecked, it will cheerfully act like this:

scala> isIntMap(Map(1 -> 1))
res19: Boolean = true
scala> isIntMap(Map("abc" -> "abc"))
res20: Boolean = true

pattern guards

scala> def guard(a: Alpha) = a match {
     | case Alpha(a, b) if a == b.doubleValue => "jinx!"
     | case Alpha(a, b) => "%s != %s".format(a, b)
     | }
guard: (a: prasinous.exercises.Alpha)java.lang.String

scala> guard(Alpha(4.0,4L))
res27: java.lang.String = jinx!

scala> guard(Alpha(4.0,5L))
res28: java.lang.String = 4.0 != 5

why case objects?

The case modifier applies to object as well: compare the definition of Some against None: there can be many instances of Some but there only needs to be a single None. Similarly, List and Nil.

final case class Some[+A](x: A) extends Option[A] {
  def isEmpty = false
  def get = x
}

case object None extends Option[Nothing] {
  def isEmpty = true
  def get = throw new NoSuchElementException("None.get")
}

extra bonus: be warned of non-exhaustive matches

sealed abstract class Fish(val scales: Int)
case object RedFish extends Fish(scales = 55)
case object BlueFish extends Fish(scales = 102)


Now the compiler can warn you is the match is non-exhaustive:

scala> def fishy(f: Fish) = f match {
     | case rf @ RedFish => rf.scales
     | }
<console>:12: warning: match is not exhaustive!
missing combination       BlueFish

       def fishy(f: Fish) = f match {
                            ^
fishy: (f: Fish)Int

enums: these are not the droids you are looking for

scala> object Color extends Enumeration {
     | val Red, Green, Blue = Value
     | }
defined module Color

scala> def color(c: Color.Value) = c match {
     | case Color.Red => "rot"
     | case Color.Blue => "blau"
     | case Color.Green => "gruen"
     | }
color: (c: Color.Value)java.lang.String

scala> def color(c: Color.Value) = c match {
     | case Color.Red => "rot"
     | case Color.Blue => "blau"
     | }
color: (c: Color.Value)java.lang.String

scala> color(Color.Green)
scala.MatchError: Green (of class scala.Enumeration$Val)