Categories
Tech

F# Friday – 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:

let getResponse talents =
    match talents with
    | 5 -> "Here are your five talents plus five more"
    | 2 -> "Here are your two talents plus two more"
    | 1 -> "Here is your one talent, which I hid"
    | _ -> "Uh, wrong story"

let response = getResponse 5
// val response : string = 
//   "Here are your five talents plus five more"

let apocryphal = getResponse 42
// val 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:

let getState temp =
    match temp with
    | x when x <= 32.0 -> "solid"
    | x when x >= 212.0 -> "gas"
    | _ -> "liquid"

let atRoomTemp = getState 70.0
// val atRoomTemp : string = "liquid"

let atSouthPole = getState -70.6
// val atSouthPole : string = "solid"

Putting a condition on a match with the when 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:

let getState2 temp =
    match temp with
    | x when 32.0 > x && x < 212.0 -> "liquid"
    | x when x >= 212.0 -> "gas"
    | _ -> "solid"
let onHotSummerDay = getState2 98.5
// val onHotSummerDay : string = "liquid"

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

let getState3 temp =
    let freezingPoint = 32.0
    let boilingPoint = 212.0
    match temp with
    | x when x <= freezingPoint -> "solid"
    | x when x >= boilingPoint -> "gas"
    | _ -> "liquid"
let inDeathValley = getState3 134.0
// val 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:

let execute command id value =
    let v = defaultArg value ""
    match command with
    | "add" -> sprintf "Added %d: %s" id v
    | "remove" -> sprintf "Removed %d" id
    | "update" -> sprintf "Updated %d: %s" id v
    | _ -> sprintf "Illegal command: %s" command

let added = execute "add" 42 (Some "foo")
// val added : string = "Added 42: foo"

let removed = execute "remove" 42 None
// val removed : string = "Removed 42"

let updated = execute "update" 42 (Some "bar")
// val updated : string = "Updated 42: bar"

let wowbanged = execute "wowbang" 73 None
// val 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:

let execute2 command id value =
    let v = defaultArg value ""
    match command with
    | "create"
    | "add" -> sprintf "Added %d: %s" id v
    | "delete"
    | "remove" -> sprintf "Added %d" id
    | "change"
    | "update" -> sprintf "Added %d: %s" id v
    | _ -> sprintf "Illegal command: %s" command
let created = execute2 "create" 84 (Some "baz")
// val created : string = "Added 84: baz"

Here Are Your Options

Perhaps you have a user variable that is a string option. 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:

let greet user =
    match user with
    | Some name -> sprintf "Welcome back, %s!" name
    | None -> "Hello, dear guest! Please sign in!"

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

let generic = greet None
// val 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:

let greet2 user =
    if Option.isSome user then
        let name = Option.get user
        sprintf "Welcome back, %s!" 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:

let getProducer chars =
    match chars with
    | ("Tom", "Jerry") -> "Hanna-Barbera"
    | ("Bugs", "Daffy") -> "Warner Brothers"
    | ("Mickey", _) -> "Disney"
    | (x, "Buzz") -> sprintf "Pixar with %s and Buzz" x
    | _ -> "other"

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

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

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

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

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

let prod6 = getProducer ("Ren", "Stimpy")
// val 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:

let report (guests : obj) =
    match guests with
    | :? string as guest -> 
        sprintf "Our guest: %s" guest
    | :? (string array) as all -> 
        (all |> String.concat ", ")
        |> sprintf "Our guests: %s"
    | :? int as count -> 
        sprintf "We have %d guests" count
    | _ -> "Huh?"

let one = report "Brad"
// val one : string = "Our guest: Brad"

let many = report [|"Me"; "Myself"; "I"|]
// val many : string = "Our guests: Me, Myself, I"

let count = report 13
// val count : string = "We have 13 guests"

let stumped = report 3.14
// val stumped : string = "Huh?"

As you can see, you can choose from any number of types to match on and take action accordingly. Note that if the input argument does not contain a type annotation, such as (guests : obj) above, the compiler refuses to compile.

3 replies on “F# Friday – Pattern Matching, Part 1”

Nice writeup. Would’ve been 100% complete with the inclusion of matching the value of variables.

Thank you, @Adrian. Good catch. I’ll try to get around to amending the post one day soon.

Leave a Reply

Your email address will not be published.

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