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.