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:
Created a discriminated union,
NameValidationResult
Created a function that implements validation logic,
validateName
Matched on the result of
validateName
(match validateName name with
)Not matched on
name
With active patterns, you can:
You can combine the two steps of creating discriminated union and function
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"
Active patterns allow you to simplify the whole process
There is no need to separately create the discriminated union
The function name is replaced with
(|<cases>|)
Such functions can return one of the cases that the function is tied-up with
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
Active patterns offer the dynamism that regular pattern matching can't
The objective of this article is to give you an idea of what can be achieved with active patterns
Later we will see complex use cases where active patterns are used
If you have reached so far, congratulations.
Keep reading!