Beyond Constant Pattern Matching

Pattern matching is a vital aspect of functional programming. It can make our code more expressive by eliminating noise. We’ll look into a few more patterns that give us the power and flexibility to create readable match expressions.

OR Pattern

The OR pattern is used to group multiple patterns which return the same result. This can be considered a short hand for multiple patterns. A simple match expression verifying a number is below 10 and prime can be written multiple ways.

// Return value for each number
let isPrimeBelow10 = match number with
                     | 2 -> true
                     | 3 -> true
                     | 5 -> true
                     | 7 -> true
                     | _ -> false

// Using the OR pattern
let isPrimeBelow10 = match number with
                     | 2 | 3 | 5 | 7 -> true
                     | _             -> false

The second version of isPrimeBelow10 is easier to reason about as it declares the patterns 2, 3, 5, and 7 are grouped to the same result. The noise of all the extra -> true has been removed. This gives the reader a visual cue about the relationships of the patterns as well as reduces duplicate code.

Guards

Guards are used to match on patterns that require more than a constant. We use the when keyword followed by an expression which evaluates to a boolean. If the guard expression evaluates to true then that pattern matches. Let’s say we want to classify a value as normal if it is within 100 to 200. That would take a lot of patterns if we used the OR pattern exclusively. It also doesn’t solve the situation if we wanted to indicate whether the value was high or low. We can use guards to accomplish our task.

let withinNormals number = match number with
                           | x when x > 200 -> High
                           | x when x < 100 -> Low
                           | _              -> Normal

We have three patterns with two of them using guards. The first pattern binds number to the label x and evaluates x > 200. If it evaluates to true, the match returns High. The second pattern does something similar, with the exception of evaluating x < 100. The final pattern is a wildcard which returns if the previous patterns are not matched.

Guards allow us to do more than just match on constants. We can expand our withinNormals to be more reusable.

let withinNormals (lowLimit, highLimit) number = 
    match number with
    | x when x > highLimit -> High
    | x when x < lowLimit  -> Low
    | _                    -> Normal

We have the same three patterns but this time we are using the arguments passed in determine the lower and upper limit. Guards let us do more than match on constant values, they give us the flexibility to create reusable patterns.

Deconstruct

Deconstruction allow the pattern to focus on what is being pattern matched. We left off Pattern Matching with Constants with a pattern matched version of Fizz Buzz. Here is the implementation again.

let printFizzBuzz number = match (number % 3, number % 5) with
                           | (0, 0) -> printfn "FizzBuzz"
                           | (0, _) -> printfn "Fizz"
                           | (_, 0) -> printfn "Buzz"
                           | _      -> printfn "%d" number

The printFizzBuzz function takes a number and creates a tuple of the results of number modulus 3 and 5. Our patterns deconstruct the tuple and match those results with constants.

We can even completely ignore values from deconstructed objects. Here we only care about the third value in the tuple. We are using the patterns to deconstruct the tuple while ignoring the first two values in the tuple. Our patterns are only focused on the values important to it.

match tupleOf3 with
| (_,_,0)            -> "0"
| (_,_,1)            -> "1"
| (_,_,x) when x < 0 -> "Negative"
| _                  -> "Larger than 1"  

Summary

The OR pattern can be used to group patterns together which have the same return value. Redundant code can be eliminated with the use of the OR pattern. Guards give match expressions the ability to compare values in patterns and not restrict us to constants. Deconstruction let patterns focus on what is being matched instead of dealing with the whole object.