Categories
Tech

Scala Saturday – Pattern Matching, Part 1

A terribly useful technique in functional programming is pattern matching. Pattern matching is simply a form of conditional logic, like an if-else expression or (in other languages) a switch statement, but quite a bit more powerful and flexible. With pattern matching, you can perform matches and take action based on simple values, such as integers, but also on complex types and the values of their members. Here are some examples using the simpler types, and a future post(s) will illustrate how to match on more complex types.

On the Whole (Numbers)

Pattern matching on integers is very straightforward. Just use literals:

def getResponse(talents: Int) = talents match {
  case 5 => "Here are your five talents plus five more"
  case 2 => "Here are your two talents plus two more"
  case 1 => "Here is your one talent, which I hid"
  case _ => "Uh, wrong story"
}

val response = getResponse(5)
// response: String = 
//   Here are your five talents plus five more

val apocryphal = getResponse(42)
// apocryphal: String = Uh, wrong story

Of course, the set of all possible integers is very large. Our example here only covers three specific values: 1, 2, and 5. To cover every other case that you don’t specify explicitly, use the wildcard pattern _, the underscore. That’s how we could handle 42. Or 103. Or −7. Or any other integer.

Getting to the (Floating) Point

Floating point numbers are difficult to pin down because of the margin of error, so you don’t typically match on a specific number. Usually you match on ranges. Pattern matching allows for that, too:

def getState(temp: Double) = temp match {
  case x if x <= 32.0 => "solid"
  case x if x >= 212.0 => "gas"
  case _ => "liquid"
}

val atRoomTemp = getState(70.0)
// atRoomTemp: String = liquid

val atSouthPole = getState(-70.6)
// atSouthPole: String = solid

Putting a condition on a match with the if keyword like that is called a guard. Guards are simply Boolean expressions. You could alternatively write the “liquid” step in getState with a compound Boolean expression:

def getState2(temp: Double) = temp match {
  case x if 32.0 > x && x < 212.0 => "liquid"
  case x if x >= 212.0 => "gas"
  case _ => "solid"
}
  
val onHotSummerDay = getState2(98.5)
// onHotSummerDay: String = solid

In one more rewrite of getState, note that it is possible to use variables, not just literals, in guards:

def getState3(temp: Double) = {
  val freezingPoint = 32.0
  val boilingPoint = 212.0
  temp match {
    case x if x <= freezingPoint => "solid"
    case x if x >= boilingPoint => "gas"
    case _ => "liquid"
  }
}

val inDeathValley = getState3(134.0)
// inDeathValley: String = liquid

Stringly Typed Interfaces

Perhaps your application executes commands, and those commands are specified by names, that is, strings. (Some folks jokingly call them “stringly typed interfaces.”) Pattern matching is perfect for this task:

def execute(command: String, id: Int, value: String = "") =
  command match {
    case "add" => s"Added ${id}: ${value}"
    case "remove" => s"Added ${id}"
    case "update" => s"Added ${id}: ${value}"
    case _ => s"Illegal command: ${command}"
  }

val added = execute("add", 42, "foo")
// added: String = Added 42: foo

val updated = execute("update", 42, "bar")
// updated: String = Added 42: bar

val removed = execute("remove", 42)
// removed: String = Added 42

val wowbanged = execute("wowbang", 73)
// wowbanged: String = Illegal command: wowbang

Perhaps you want to allow some flexibility in your command names. For example, another name for “add” could be “create.” Pattern matching expressions allow you to stack multiple conditions for which you want to take the same action using a pipe (|):

def execute2(command: String, id: Int, value: String = "") =
  command match {
    case "add" | "create" => s"Added ${id}: ${value}"
    case "remove" | "delete" => s"Added ${id}"
    case "update" | "change" => s"Added ${id}: ${value}"
    case _ => s"Illegal command: ${command}"
  }

val created = execute2("create", 84, "baz")
// created: String = Added 84: baz

Here Are Your Options

Perhaps you have a user variable that is an Option[String]. If the user is logged in, user is a Some; otherwise the user is an unauthenticated guest. You’d like to generate a greeting based on whether the user is logged in or not:

def greet(user: Option[String]) = user match {
  case Some(name) => s"Welcome back, ${name}!"
  case None => "Hello, dear guest! Please sign in!"
}

val personal = greet(Some("bcollins"))
// personal: String = Welcome back, bcollins!

val generic = greet(None)
// generic: String = 
//   Hello, dear guest! Please sign in!

Notice how the match expression can, in the case of the Some, unpack the value for you and assign it to a variable, name in this case. You don’t have to do it yourself.

Now given that Option is a binary choice, you may be tempted to think that an if-else block is probably a better, um, option. In some simple cases it may be, but in this case, you want to generate a message that differs by more than just the username. Look at what it takes to get the same results with an if-else block as the match expression above:

def greet2(user: Option[String]) =
  if (user.isDefined) {
    val name = user.get
    s"Welcome back, ${name}!"
  } else {
    "Hello, dear guest! Please sign in!"
  }

Now that’s not too bad, but compare that to the original version that uses the match expression. The match expression is just cleaner: It is very easy to see what the conditions are, and you don’t have to clutter the code by unpacking the Some value yourself.

Bearing with the Tuples of the Week

Pattern matching expressions can unpack tuples so that you can match on all values or individual values:

def getProducer(chars: (String, String)) =
  chars match {
    case ("Tom", "Jerry") => "Hanna-Barbera"
    case ("Bugs", "Daffy") => "Warner Brothers"
    case ("Mickey", _) => "Disney"
    case (x, "Buzz") => s"Pixar with ${x} and Buzz"
    case _ => "other"
  }

val prod = getProducer("Tom", "Jerry")
// prod: String = Hanna-Barbera

val prod2 = getProducer("Mickey", "Minnie")
// prod2: String = Disney

val prod3 = getProducer("Mickey", "Donald")
// prod3: String = Disney

val prod4 = getProducer("Woody", "Buzz")
// prod4: String = Pixar with Woody and Buzz

val prod5 = getProducer("Andy", "Buzz")
// prod5: String = Pixar with Andy and Buzz

val prod6 = getProducer("Ren", "Stimpy")
// prod6: String = other

As you can see in the “Disney” line, you don’t care what the second element of the tuple is, so you throw it away with the wildcard pattern. On the other hand, on the “Pixar …” line, you want to capture the first element in the matched tuple and use it in the result. So you just give it a variable name, and the compiler assigns the value to the variable for you, just like with Some(name) above.

What’s Your Type?

Finally, pattern matching expressions can match even on different types and take action based on the type of the input:

def report(guests: Any) = guests match {
  case guest: String =>
    s"Our guest: ${guest}"
  case all: Array[String] =>
    s"Our guests: ${all.mkString(", ")}"
  case count: Int =>
    s"We have ${count} guests"
  case _ => "Huh?"
}

val one = report("Brad")
// one: String = Our guest: Brad

val many = report(Array("Me", "Myself", "I"))
// many: String = Our guests: Me, Myself, I

val count = report(13)
// count: String = We have 13 guests

val stumped = report(3.14)
// stumped: String = Huh?

As you can see, you can choose from any number of types to match on and take action accordingly.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.