Introducing Active Patterns

We’ve been working with pattern matching in F# for awhile and we are ready to dive into Active Patterns. There are 16 different patterns we can match on right out of the box. For the cases where the base patterns aren’t enough we can use Active Patterns to pick up the slack. There are four kinds of active patterns. We’ll begin by looking at two kinds of active patterns.

Single-Case Active Patterns

This kind of active pattern isn’t used all that often. A Single-Case Active Pattern can be thought of as a view or a subset of data from an object. This gives us the option of matching on some of the data or transforming the data into something useful.

#r "System.Windows.Forms"
open System.Windows.Forms

let (|Position|) (ctrl : Control) = ctrl.Left, ctrl.Top

let (|Size|) (ctrl : Control) = ctrl.Width, ctrl.Height

let (|PositionAndSize|) (ctrl : Control) = 
    match ctrl, ctrl with
    | Position p, Size s -> p, s

The System.Windows.Forms.Control class has over 30 properties. We can group related properties together using single-case active patterns. We’ve defined some patterns which can be used to match on properties we may be interested. We’ve created active patterns for Position, Size, and a combination of the two.

We can use our active patterns in other functions to make our code more expressive.

let printPosition ctrl =
    match ctrl with
    | Position (x,y) -> printfn "Left = %d, Top = %d" x y

We can use a pattern matching shorthand to make the function more concise.

let printPosition = function 
    | Position (x,y) -> printfn "Left = %d, Top = %d" x y

Multicase Active Patterns

The Multicase Active Pattern is more commonly used than the single-case active pattern. We partition the input domain into a known set of possible values. The multicast active pattern is a closed set of values and therefore a successful match must be returned. As of F# version 4.1 there is an upper limit of 7 values you can define in a multicase active pattern.

Let’s categorize scores in the game of darts. We can place a score into a finite set of possibilities. An invalid score is anything less than 0 or more than 180. We’ll mark scores of 150 and 180 as being special. Anything between 150 and 180 will be a high ton. A score between 100 and 150 will be classified a low ton. The rest will be standard points.

We’ll create a multicase active pattern to improve the readability of our pattern match.

let (|LowTon|Ton50|HighTon|Ton80|Points|Invalid|) x =
    if x < 0 || 180 < x then Invalid
    elif x = 150 then Ton50
    elif x = 180 then Ton80
    elif x < 100 then Points x
    elif x < 150 then LowTon x
    else HighTon x

This example isn't terribly unreadable. Creating the active pattern does allow us to define the potentially misunderstood code once and then use the clear and concise patterns throughout the rest of the application.

let printScore = function
  | Points pts  -> printfn "%d points." pts
  | LowTon pts  -> printfn "Low ton of %d points." pts
  | Ton50       -> printfn "One hundred and fifty!"
  | HighTon pts -> printfn "Wahoo! High ton of %d points!" pts
  | Ton80       -> printfn "Onnneeee Hundred and eightyyyyyy!!!!"
  | Invalid     -> printfn "Score must be between 0 and 180."

Summary

A single-case active pattern is good for creating a view of a complex or large model. We can define subsets of properties or values we require from the model which can be pattern matched. The single-case active pattern has a limited use case and isn't used very often. Multicase active patterns are good when your input can be categorized into a finite set of values. Multicase active patterns are used quite regularly in combination with other kinds of active patterns.