Basics 24 - Active Patterns

This article is a continuation of Basics 09 - Pattern Matching (Basics).

Learning-FSharp/Ch24-ActivePatterns/Program.fs

Check out all the comments that are placed in the source file.

Before We Begin

Let's take a simple example to understand what active patterns offer.

You need to validate a given name. For simplicity's sake, we are checking that the given name isn't an empty string.

Here is the first, simple version:

let name = "Whatever"

match name with
| value when value.Length = 0 -> false
| _ -> true
|> printfn "%A"

The match expression with length comparison isn't the most readable, also the block isn't reusable. You can create a dedicated discriminated union and function to handle this:

// Discriminated union for indicating name validation result
type NameValidationResult =
    | NameValidationOk
    | NameValidationNotOk

// Name validation function
let validateName (name: string) =
    if name.Length = 0 then
        NameValidationNotOk
    else
        NameValidationOk

let name = "Whatever"

match validateName name with
| NameValidationNotOk -> false
| NameValidationOk -> true
|> printfn "%A"

Active Patterns - The Simple Version

In the above-given example, we have:

  1. Created a discriminated union, NameValidationResult

  2. Created a function that implements validation logic, validateName

  3. Matched on the result of validateName (match validateName name with)

  4. Not matched on name

With active patterns, you can:

  1. You can combine the two steps of creating discriminated union and function

  2. Apply pattern matching on the name instead of the result of the function

Here's the active pattern version of what we previously implemented:

// Name validation active pattern
let (|InvalidName|ValidName|) (name: string) =
    if name.Length = 0 then InvalidName else ValidName

let name = "Whatever"

match name with
| InvalidName -> false
| ValidName -> true
|> printfn "%A"
  1. Active patterns allow you to simplify the whole process

  2. There is no need to separately create the discriminated union

  3. The function name is replaced with (|<cases>|)

  4. Such functions can return one of the cases that the function is tied-up with

  5. The code match name with is translated into a function call, match <func-name> name with

Beyond Simplicity

As illustrated in the example above, you can simplify the whole process. However, that's not the real advantage of active patterns.

The problem or limitation with regular pattern matching is that it is hard-baked into the code. It doesn't offer dynamism and flexibility of functions.

Active patterns, which are functions, offer much more. Let's see various examples to see how such dynamism can be achieved.

Example 1 - Number to Month

This is an example of a number-to-month conversion. If the given number can be converted to a month, case ValidMonth is used, otherwise, InvalidMonth is used.

// Representation of months
type Month =
    | Jan | Feb | Mar | Apr
    | May | Jun | Jul | Aug
    | Sep | Oct | Nov | Dec

// Given number to month
let (|ValidMonth|InvalidMonth|) num =
    let m = [ Jan; Feb; Mar; Apr; May; Jun; Jul; Aug; Sep; Oct; Nov; Dec ]

    if num >= 1 && num <= m.Length then
        ValidMonth m[num - 1]
    else
        InvalidMonth

match 1 with
|ValidMonth m-> $"Month: {m}"
|InvalidMonth -> "Sorry. Should be between 1 and 12."
|> printfn "%A" // "Month: Jan"

match 13 with
|ValidMonth m-> $"Month: {m}"
|InvalidMonth -> "Sorry. Should be between 1 and 12."
|> printfn "%A" // "Sorry. Should be between 1 and 12."

Example 2 - Partial Active Pattern

Partial active patterns are shorthand for combining active patterns with option.

Include _ as the last case to make an active pattern partial.

Take example of (|GreaterThan100|_|). GreaterThan100 is int option here. From the function, you simply return Some <value>.

// Checks if the number is > than 100
let (|GreaterThan100|_|) num = if num > 100 then Some num else None

// Checks if the number is < than 100
let (|LessThan100|_|) num = if num < 100 then Some num else None

// Compares the given number with 100
let compareNumWith100 num =
    match num with
    | GreaterThan100 x -> $"{x} is greater than 100."
    | LessThan100 x -> $"{x} is less than 100."
    | _ -> "Seems number is 100."

compareNumWith100 1 |> printfn "%A" // "1 is less than 100."
compareNumWith100 101 |> printfn "%A" // "101 is greater than 100."
compareNumWith100 100 |> printfn "%A" // "Seems number is 100."

Example 3 - Partial Application

This is an example of partial application.

In the last example, we created 2 functions, (|GreaterThan100|_|) and (|LessThan100|_|). Both these functions are specialized. To make generalized versions, we create (|GreaterThan|_|) and (|LessThan|_|) in this example.

Once the generalized versions are ready, we can use the partial application, like with any function, to make specialized versions. (|GreaterThan5000|_|) and (|LessThan5000|_|) are created by the partial application of (|GreaterThan|_|) and (|LessThan|_|) respectively.

// Checks if the number is > than compareWith
// General purpose
let (|GreaterThan|_|) compareWith num =
    if num > compareWith then Some num else None

// Checks if the number is < than compareWith
// General purpose
let (|LessThan|_|) compareWith num =
    if num < compareWith then Some num else None

// Partial application of (|GreaterThan|_|)
// Checks if the number is > than 5000
// Specialized
let (|GreaterThan5000|_|) = (|GreaterThan|_|) 5000

// Partial application of (|LessThan5000|_|)
// Checks if the number is < than 5000
// Specialized
let (|LessThan5000|_|) = (|LessThan|_|) 5000

// Compares the given number with 5000
let compareNumWith5000 num =
    match num with
    | GreaterThan5000 x -> $"{x} is greater than 5000."
    | LessThan5000 x -> $"{x} is less than 5000."
    | _ -> "Seems number is 5000."

compareNumWith5000 1 |> printfn "%A" // "1 is less than 5000."
compareNumWith5000 5001 |> printfn "%A" // "5001 is greater than 5000."
compareNumWith5000 5000 |> printfn "%A" // "Seems number is 5000."

Concluding Remarks

  1. Active patterns offer the dynamism that regular pattern matching can't

  2. The objective of this article is to give you an idea of what can be achieved with active patterns

  3. Later we will see complex use cases where active patterns are used

If you have reached so far, congratulations.

Keep reading!