Patterns, Patterns, Everywhere - The Book of F#: Breaking Free with Managed Functional Programming (2014)

The Book of F#: Breaking Free with Managed Functional Programming (2014)

Chapter 7. Patterns, Patterns, Everywhere

Pattern matching is one of F#’s most powerful features. Patterns are so ingrained within the language that they’re employed by many of the constructs you’ve already seen, like let bindings, try...with expressions, and lambda expressions. In this chapter, you’ll learn about match expressions, predefined pattern types, and creating your own patterns with active patterns.

Match Expressions

Although F# allows imperative style branching through if expressions, they can be difficult to maintain, particularly as the conditional logic’s complexity increases. Match expressions are F#’s primary branching mechanism.

On the surface, many match expressions resemble C#’s switch or Visual Basic’s Select Case statements, but they’re significantly more powerful. For instance, while switch and Select Case operate against only constant values, match expressions select an expression to evaluate according to which pattern matches the input. At their most basic, match expressions take the following form:

match ①test-expression with

| ②pattern1 -> ③result-expression1

| ④pattern2 -> ⑤result-expression2

| ...

In the preceding syntax, the expression at ① is evaluated and sequentially compared to each pattern in the expression body until a match is found. For example, if the result satisfies the pattern at ②, the expression at ③ is evaluated. Otherwise, the pattern at ④ is tested and, if it matches, the expression at ⑤ is evaluated, and so on. Because match expressions also return a value, each result expression must be of the same type.

The fact that patterns are matched sequentially has consequences for how you structure your code; you must organize your match expressions such that the patterns are listed from most to least specific. If a more general pattern is placed ahead of more specific patterns in a way that prevents any subsequent patterns from being evaluated, the compiler will issue a warning for each affected pattern.

Match expressions can be used with a wide variety of data types including (but not limited to) numbers, strings, tuples, and records. For example, here’s a function with a simple match expression that works with a discriminated union:

let testOption opt =

match opt with

| Some(v) -> printfn "Some: %i" v

| None -> printfn "None"

In this snippet, opt is inferred to be of type int option, and the match expression includes patterns for both the Some and None cases. When the match expression evaluates, it first tests whether opt matches Some. If so, the pattern binds the value from Some into v, which is then printed when the result expression is evaluated. Likewise, when None matches, the result expression simply prints out "None".

Guard Clauses

In addition to matching disparate values against patterns, you can further refine each case through guard clauses, which allow you to specify additional criteria that must be met to satisfy a case. For instance, you can use guard clauses (by inserting when followed by a condition) to distinguish between positive and negative numbers like so:

let testNumber value =

match value with

| ①v when v < 0 -> printfn "%i is negative" v

| ②v when v > 0 -> printfn "%i is positive" v

| _ -> printfn "zero"

In this example, we have two cases with identical patterns but different guard clauses. Even though any integer will match any of the three patterns, the guard clauses on patterns ① and ② cause matching to fail unless the captured value meets their criteria.

You can combine multiple guard clauses with Boolean operators for more complex matching logic. For instance, you could construct a case that matches only positive, even integers as follows:

let testNumber value =

match value with

| v when v > 0 && v % 2 = 0 -> printfn "%i is positive and even" v

| v -> printfn "%i is zero, negative, or odd" v

Pattern-Matching Functions

There is an alternative match expression syntax called a pattern-matching function. With the pattern-matching function syntax, the match...with portion of the match expression is replaced with function like this:

> let testOption =

function

| Some(v) -> printfn "Some: %i" v

| None -> printfn "None";;

val testOption : _arg1:int option -> unit

As you can see from the signature in the output, by using the pattern-matching function syntax, we bind testOption to a function that accepts an int option (with the generated name _arg1) and returns unit. Using the function keyword this way is a convenient shortcut for creating a pattern-matching lambda expression and is functionally equivalent to writing:

fun x ->

match x with

| Some(v) -> printfn "Some: %i" v

| None -> printfn "None";;

Because pattern-matching functions are just a shortcut for lambda expressions, passing match expressions to higher-order functions is trivial. Suppose you want to filter out all of the None values from a list of optional integers. You might consider passing a pattern-matching function to theList.filter function like this:

[ Some 10; None; Some 4; None; Some 0; Some 7 ]

|> List.filter (function | Some(_) -> true

| None -> false)

When the filter function is executed, it will invoke the pattern-matching function against each item in the source list, returning true when the item is Some(_), or false when the item is None. As a result, the list created by filter will contain only Some 10, Some 4, Some 0, andSome 7.

Exhaustive Matching

When a match expression includes patterns such that every possible result of the test expression is accounted for, it is said to be exhaustive, or covering. When a value exists that isn’t covered by a pattern, the compiler issues a warning. Consider what happens when we match against an integer but cover only a few cases.

> let numberToString =

function

| 0 -> "zero"

| 1 -> "one"

| 2 -> "two"

| 3 -> "three";;

function

--^^^^^^^^

stdin(4,3): warning FS0025: Incomplete pattern matches on this expression. For

example, the value '4' may indicate a case not covered by the pattern(s).

val numberToString : _arg1:int -> string

Here you can see that if the integer is ever anything other than 0, 1, 2, or 3, it will never be matched. The compiler even provides an example of a value that might not be covered—four, in this case. If numberToString is called with a value that isn’t covered, the call fails with aMatchFailureException:

> numberToString 4;;

Microsoft.FSharp.Core.MatchFailureException: The match cases were incomplete

at FSI_0025.numberToString(Int32 _arg1)

at <StartupCode$FSI_0026>.$FSI_0026.main@()

Stopped due to error

To address this problem, you could add more patterns, trying to match every possible value, but many times (such as with integers) matching every possible value isn’t feasible. Other times, you may care only about a few cases. In either scenario, you can turn to one of the patterns that match any value: the Variable pattern or the Wildcard pattern.

Variable Patterns

Variable patterns are represented with an identifier and are used whenever you want to match any value and bind that value to a name. Any names defined through Variable patterns are then available for use within guard clauses and the result expression for that case. For example, to makenumberToString exhaustive, you could revise the function to include a Variable pattern like this:

let numberToString =

function

| 0 -> "zero"

| 1 -> "one"

| 2 -> "two"

| 3 -> "three"

① | n -> sprintf "%O" n

When you include a Variable pattern at ①, anything other than 0, 1, 2, or 3 will be bound to n and simply converted to a string.

The identifier defined in a Variable pattern should begin with a lowercase letter to distinguish it from an Identifier pattern. Now, invoking numberToString with 4 will complete without error, as shown here:

> numberToString 4;;

val it : string = "4"

The Wildcard Pattern

The Wildcard pattern, represented as a single underscore character (_), works just like a Variable pattern except that it discards the matched value rather than binding it to a name.

Here’s the previous numberToString implementation revised with a Wildcard pattern. Note that because the matched value is discarded, we need to return a general string instead of something based on the matched value.

let numberToString =

function

| 0 -> "zero"

| 1 -> "one"

| 2 -> "two"

| 3 -> "three"

| _ -> "unknown"

Matching Constant Values

Constant patterns consist of hardcoded numbers, characters, strings, and enumeration values. You’ve already seen several examples of Constant patterns, but to reiterate, the first four cases in the numberToString function that follows are all Constant patterns.

let numberToString =

function

| 0 -> "zero"

| 1 -> "one"

| 2 -> "two"

| 3 -> "three"

| _ -> "..."

Here, the numbers 0 through 3 are explicitly matched and return the number as a word. All other values fall into the wildcard case.

Identifier Patterns

When a pattern consists of more than a single character and begins with an uppercase character, the compiler attempts to resolve it as a name. This is called an Identifier pattern and typically refers to discriminated union cases, identifiers decorated with LiteralAttribute, or exception names (as seen in a try...with block).

Matching Union Cases

When the identifier is a discriminated union case, the pattern is called a Union Case pattern. Union Case patterns must include a wildcard or identifier for each data item associated with that case. If the case doesn’t have any associated data, the case label can appear on its own.

Consider the following discriminated union that defines a few shapes:

type Shape =

| Circle of float

| Rectangle of float * float

| Triangle of float * float * float

From this definition, it’s trivial to define a function that uses a match expression to calculate the perimeter of any of the included shapes. Here is one possible implementation:

let getPerimeter =

function

| Circle(r) -> 2.0 * System.Math.PI * r

| Rectangle(w, h) -> 2.0 * (w + h)

| Triangle(l1, l2, l3) -> l1 + l2 + l3

As you can see, each shape defined by the discriminated union is covered, and the data items from each case are extracted into meaningful names like r for the radius of a circle or w and h for the width and height of a rectangle, respectively.

Matching Literals

When the compiler encounters an identifier defined with LiteralAttribute used as a case, it is called a Literal pattern but is treated as though it were a Constant pattern.

Here is the numberToString function revised to use a few Literal patterns instead of Constant patterns:

[<LiteralAttribute>]

let Zero = 0

[<LiteralAttribute>]

let One = 1

[<LiteralAttribute>]

let Two = 2

[<LiteralAttribute>]

let Three = 3

let numberToString =

function

| Zero -> "zero"

| One -> "one"

| Two -> "two"

| Three -> "three"

| _ -> "unknown"

Matching Nulls

When performing pattern matching against types where null is a valid value, you’ll typically want to include a Null pattern to keep any nulls as isolated as possible. Null patterns are represented with the null keyword.

Consider this matchString pattern-matching function:

> let matchString =

function

| "" -> None

| v -> Some(v.ToString());;

val matchString : _arg1:string -> string option

The matchString function includes two cases: a Constant pattern for the empty string and a Variable pattern for everything else. The compiler was happy to create this function for us without warning about incomplete pattern matches, but there’s a potentially serious problem: null is a valid value for strings, but the Variable pattern matches any value, including null! Should a null string be passed to matchString, a NullReferenceException will be thrown when the ToString method is called on v because the Variable pattern matches null and therefore sets v tonull, as shown here:

> matchString null;;

System.NullReferenceException: Object reference not set to an instance of an object.

at FSI_0070.matchString(String _arg1) in C:\Users\Dave\AppData\Local\Temp\~vsE434.fsx:line 68

at <StartupCode$FSI_0071>.$FSI_0071.main@()

Stopped due to error

Adding a Null pattern before the Variable pattern will ensure that the null value doesn’t leak into the rest of the application. By convention, Null patterns are typically listed first, so that’s the approach shown here with the null and empty string patterns combined with an OR pattern:

let matchString =

function

| null

| "" -> None

| v -> Some(v.ToString())

Matching Tuples

You can match and decompose a tuple to its constituent elements with a Tuple pattern. For instance, a two-dimensional point represented as a tuple can be decomposed to its individual x- and y-coordinates with a Tuple pattern within a let binding like this:

let point = 10, 20

let x, y = point

In this example, the values 10 and 20 are extracted from point and bound to the x and y identifiers, respectively.

Similarly, you can use several Tuple patterns within a match expression to perform branching based upon the tupled values. In keeping with the point theme, to determine whether a particular point is located at the origin or along an axis, you could write something like this:

let locatePoint p =

match p with

| ① (0, 0) -> sprintf "%A is at the origin" p

| ② (_, 0) -> sprintf "%A is on the x-axis" p

| ③ (0, _) -> sprintf "%A is on the y-axis" p

| ④ (x, y) -> sprintf "Point (%i, %i)" x y

The locatePoint function not only highlights using multiple Tuple patterns but also shows how multiple pattern types can be combined to form more complex branching logic. For instance, ① uses two Constant patterns within a Tuple pattern, while ② and ③ each use a Constant pattern and a Wildcard pattern within a Tuple pattern. Finally, ④ uses two Variable patterns within a Tuple pattern.

Remember, the number of items in a Tuple pattern must match the number of items in the tuple itself. For instance, attempting to match a Tuple pattern containing two items with a tuple containing three items will result in a compiler error because the underlying types are incompatible.

Matching Records

Record types can participate in pattern matching through Record patterns. With Record patterns, individual record instances can be matched and decomposed to their individual values.

Consider the following record type definition based on a typical American name:

type Name = { First : string; Middle : string option; Last : string }

In this record type, both the first and last names are required, but the middle name is optional. You can use a match expression to format the name according to whether a middle name is specified like so:

let formatName =

function

| { First = f; Middle = Some(m); Last = l } -> sprintf "%s, %s %s" l f m

| { First = f; Middle = None; Last = l } -> sprintf "%s, %s" l f

Here, both patterns bind the first and last names to the identifiers f and l, respectively. But more interesting is how the patterns match the middle name against union cases for Some(m) and None. When the match expression is evaluated against a Name that includes a middle name, the middle name is bound to m. Otherwise, the match fails and the None case is evaluated.

The patterns in the formatName function extract each value from the record, but Record patterns can operate against a subset of the labels, too. For instance, if you want to determine only whether a name includes a middle name, you could construct a match expression like this:

let hasMiddleName =

function

| { Middle = Some(_) } -> true

| { Middle = None } -> false

Many times, the compiler can automatically resolve which record type the pattern is constructed against, but if it can’t, you can specify the type name as follows:

let hasMiddleName =

function

| { Name.Middle = Some(_) } -> true

| { Name.Middle = None } -> false

Qualifying the pattern like this will typically be necessary only when there are multiple record types with conflicting definitions.

Matching Collections

Pattern matching isn’t limited to single values or structured data like tuples and records. F# includes several patterns for matching one-dimensional arrays and lists, too. If you want to match against another collection type, you’ll typically need to convert the collection to a list or array withList.ofSeq, Array.ofSeq, or a comparable mechanism.

Array Patterns

Array patterns closely resemble array definitions and let you match arrays with a specific number of elements. For example, you can use Array patterns to determine the length of an array like this:

let getLength =

function

| null -> 0

| [| |] -> 0

| [| _ |] -> 1

| [| _; _; |] -> 2

| [| _; _; _ |] -> 3

| a -> a |> Array.length

Ignoring the fact that to get the length of an array you’d probably forego this contrived pattern-matching example and inspect the Array.length property directly, the getLength function shows how Array patterns can match individual array elements from fixed-size arrays.

List Patterns

List patterns are similar to Array patterns except that they look like and work against F# lists. Here’s the getLength function revised to work with F# lists instead of arrays.

let getLength =

function

| [ ] -> 0

| [ _ ] -> 1

| [ _; _; ] -> 2

| [ _; _; _ ] -> 3

| lst -> lst |> List.length

Note that there’s no null case because null is not a valid value for an F# list.

Cons Patterns

Another way to match F# lists is with the Cons pattern. In pattern matching, the cons operator (::) works in reverse; instead of prepending an element to a list, it separates a list’s head from its tail. This allows you to recursively match against a list with an arbitrary number of elements.

In keeping with our theme, here’s how you could use a Cons pattern to find a collection’s length through pattern matching:

let getLength n =

① let rec len c l =

match l with

| ② [] -> c

| ③ _ :: t -> len (c + 1) t

len 0 n

This version of the getLength function is very similar to the F# list’s internal length property implementation. It defines len ①, an internal function that recursively matches against either an empty pattern ② or a Cons pattern ③. When the empty list is matched, len returns the supplied count value (c); otherwise, it makes a recursive call, incrementing the count and passing along the tail. The Cons pattern in getLength uses the Wildcard pattern for the head value because it’s not needed for subsequent operations.

Matching by Type

F# has two ways to match against particular data types: Type-Annotated patterns and Dynamic Type-Test patterns.

Type-Annotated Patterns

Type-Annotated patterns let you specify the type of the matched value. They are especially useful in pattern-matching functions where the compiler needs a little extra help determining the expected type of the function’s implicit parameter. For example, the following function is supposed to check whether a string begins with an uppercase character:

// Does not compile

let startsWithUpperCase =

function

| ① s when ② s.Length > 0 && s.[0] = System.Char.ToUpper s.[0] -> true

| _ -> false

As written, though, the startsWithUpperCase function won’t compile. Instead, it will fail with the following error:

~vsD607.fsx(83,12): error FS0072: Lookup on object of indeterminate type based

on information prior to this program point. A type annotation may be needed

prior to this program point to constrain the type of the object. This may

allow the lookup to be resolved.

The reason this fails to compile is that the guard conditions at ② rely on string properties, but those properties aren’t available because the compiler has automatically generalized the function’s implicit parameter. To fix the problem, we could either revise the function to have an explicit string parameter or we can include a type annotation in the pattern at ① like this (note that the parentheses are required):

let startsWithUpperCase =

function

| (s : string) when s.Length > 0 && s.[0] = System.Char.ToUpper s.[0] ->

true

| _ -> false

With the type annotation in place, the parameter is no longer automatically generalized, making the string’s properties available within the guard condition.

Dynamic Type-Test Patterns

Dynamic Type-Test patterns are, in a sense, the opposite of Type-Annotated patterns. Where Type-Annotated patterns force each case to match against the same data type, Dynamic Type-Test patterns are satisfied when the matched value is an instance of a particular type; that is, if you annotate a pattern to match strings, every case must match against strings. Dynamic Type-Test patterns are therefore useful for matching against type hierarchies. For instance, you might match against an interface instance but use Dynamic Type-Test patterns to provide different logic for specific implementations. Dynamic Type-Test patterns resemble the dynamic cast operator (:?>) except that the > is omitted.

The following detectColorSpace function shows you how to use Dynamic Type-Test patterns by matching against three record types. If none of the types are matched, the function raises an exception.

type RgbColor = { R : int; G : int; B : int }

type CmykColor = { C : int; M : int; Y : int; K : int }

type HslColor = { H : int; S : int; L : int }

let detectColorSpace (cs : obj) =

match cs with

| :? RgbColor -> printfn "RGB"

| :? CmykColor -> printfn "CMYK"

| :? HslColor -> printfn "HSL"

| _ -> failwith "Unrecognized"

As Patterns

The As pattern lets you bind a name to the whole matched value and is particularly useful in let bindings that use pattern matching and pattern-matching functions where you don’t have direct named access to the matched value.

Normally, a let binding simply binds a name to a value, but as you’ve seen, you can also use patterns in a let binding to decompose a value and bind a name to each of its constituent parts like this:

> let x, y = (10, 20);;

val y : int = 20

val x : int = 10

If you want to bind not only the constituent parts but also the whole value, you could explicitly use two let bindings like this:

> let point = (10, 20)

let x, y = point;;

val point : int * int = (10, 20)

val y : int = 20

val x : int = 10

Having two separate let bindings certainly works, but it’s more succinct to combine them into one with an As pattern like so:

> let x, y as point = (10, 20);;

val point : int * int = (10, 20)

val y : int = 20

val x : int = 10

The As pattern isn’t restricted to use within let bindings; you can also use it within match expressions. Here, we include an As pattern in each case to bind the matched tuple to a name.

let locatePoint =

function

| (0, 0) as p -> sprintf "%A is at the origin" p

| (_, 0) as p -> sprintf "%A is on the X-Axis" p

| (0, _) as p -> sprintf "%A is on the Y-Axis" p

| (x, y) as p -> sprintf "Point (%i, %i)" x y

Combining Patterns with AND

With AND patterns, sometimes called Conjunctive patterns, you match the input against multiple, compatible patterns by combining them with an ampersand (&). For the case to match, the input must satisfy each pattern.

Generally speaking, AND patterns aren’t all that useful in basic pattern-matching scenarios because the more expressive guard clauses are usually better suited to the task. That said, AND patterns are still useful for things like extracting values when another pattern is matched. (AND patterns are also used heavily with active patterns, which we’ll look at later.) For example, to determine whether a two-dimensional point is located at the origin or along an axis, you could write something like this:

let locatePoint =

function

| (0, 0) as p -> sprintf "%A is at the origin" p

| ① (x, y) & (_, 0) -> sprintf "(%i, %i) is on the x-axis" x y

| ② (x, y) & (0, _) -> sprintf "(%i, %i) is on the y-axis" x y

| (x, y) -> sprintf "Point (%i, %i)" x y

The locatePoint function uses AND patterns at ① and ② to extract the x and y values from a tuple when the second or first value is 0, respectively.

Combining Patterns with OR

If a number of patterns should execute the same code when they’re matched, you can combine them using an OR, or Disjunctive, pattern. An OR pattern combines multiple patterns with a vertical pipe character (|). In many ways, OR patterns are similar to fall-through cases in C#’s switchstatements.

Here, the locatePoint function has been revised to use an OR pattern so the same message can be printed for points on either axis:

let locatePoint =

function

| (0, 0) as p -> sprintf "%A is at the origin" p

| ① (_, 0) | ② (0, _) as p -> ③ sprintf "%A is on an axis" p

| p -> sprintf "Point %A" p

In this version of locatePoint, the expression at ③ is evaluated when either the pattern at ① or ② is satisfied.

Parentheses in Patterns

When combining patterns, you can establish precedence with parentheses. For instance, to extract the x and y values from a point and also match whether the point is on either axis, you could write something like this:

let locatePoint =

function

| (0, 0) as p -> sprintf "%A is at the origin" p

| (x, y) & ① ((_, 0) | (0, _)) -> sprintf "(%i, %i) is on an axis" x y

| p -> sprintf "Point %A" p

Here, you match three patterns, establishing associativity at ① by wrapping the two axis-checking patterns in parentheses.

Active Patterns

When none of the built-in pattern types do quite what you need, you can turn to active patterns. Active patterns are a special type of function definition, called an active recognizer, where you define one or more case names for use in your pattern-matching expressions.

Active patterns have many of the same characteristics of the built-in pattern types; they accept an input value and can decompose the value to its constituent parts. Unlike basic patterns, though, active patterns not only let you define what constitutes a match for each named case, but they can also accept other inputs.

Active patterns are defined with the following syntax:

let (|CaseName1|CaseName2|...|CaseNameN|) [parameters] -> expression

As you can see, the case names are enclosed between (| and |) (called banana clips) and are pipe-delimited. The active pattern definition must always include at least one parameter for the value to match and, because active recognizer functions are curried, the matched value must be the final parameter in order to work correctly with match expressions. Finally, the expression’s return value must be one of the named cases along with any associated data.

There are plenty of uses for active patterns, but a good example lies in a possible solution to the famed FizzBuzz problem. For the uninitiated, FizzBuzz is a puzzle that employers sometimes use during interviews to help screen candidates. The task at the heart of the problem is simple and often phrased thusly:

Write a program that prints the numbers from 1 to 100. But for multiples of three, print "Fizz" instead of the number; for the multiples of five, print "Buzz". For numbers that are multiples of both three and five, print "FizzBuzz".

To be clear, active patterns certainly aren’t the only (or necessarily even the best) way to solve the FizzBuzz problem. But the FizzBuzz problem—with its multiple, overlapping rules—allows us to showcase how powerful active patterns are.

We can start by defining the active recognizer. From the preceding description, we know that we need four patterns: Fizz, Buzz, FizzBuzz, and a default case for everything else. We also know the criteria for each case, so our recognizer might look something like this:

let (|Fizz|Buzz|FizzBuzz|Other|) n =

match ① (n % 3, n % 5) with

| ② 0, 0 -> FizzBuzz

| ③ 0, _ -> Fizz

| ④ _, 0 -> Buzz

| ⑤ _ -> Other n

Here we have an active recognizer that defines the four case names. The recognizer’s body relies on further pattern matching to select the appropriate case. At ①, we construct a tuple containing the modulus of n and 3 and the modulus of n and 5. We then use a series of Tuple patterns to identify the correct case, the most specific being ②, where both elements are 0. The cases at ③ and ④ match when n is divisible by 3 and n is divisible by 5, respectively. The final case, ⑤, uses the Wildcard pattern to match everything else and return Other along with the supplied number. The active pattern gets us only partway to the solution, though; we still need to print the results.

The active recognizer identifies only which case a given number meets, so we still need a way to translate each case to a string. We can easily map the cases with a pattern-matching function like this:

let fizzBuzz =

function

| Fizz -> "Fizz"

| Buzz -> "Buzz"

| FizzBuzz -> "FizzBuzz"

| Other n -> n.ToString()

The preceding fizzBuzz function uses basic pattern matching, but instead of using the built-in patterns, it uses cases defined by the active recognizer. Note how the Other case includes a Variable pattern, n, to hold the number associated with it.

Finally, we can complete the task by printing the results. We could do this in an imperative style, but because a functional approach is more fun let’s use a sequence like this:

seq { 1..100 }

|> Seq.map fizzBuzz

|> Seq.iter (printfn "%s")

Here, we create a sequence containing the numbers 1 through 100 and pipe it to Seq.map, which creates a new sequence containing the strings returned from fizzBuzz. The resulting sequence is then piped on to Seq.iter to print each value.

Partial Active Patterns

As convenient as active patterns are, they do have a few drawbacks. First, each input must map to a named case. Second, active patterns are limited to seven named cases. If your situation doesn’t require mapping every possible input or you need more than seven cases, you can turn to partial active patterns.

Partial active patterns follow the same basic structure as complete active patterns, but instead of a list of case names they include only a single case name followed by an underscore. The basic syntax for a partial active pattern looks like this:

let (|CaseName|_|) [parameters] = expression

The value returned by a partial active pattern is a bit different than complete active patterns, too. Instead of returning the case directly, partial active patterns return an option of the pattern’s type. For example, if you have a partial active pattern for Fizz, the expression needs to return eitherSome(Fizz) or None. As far as your match expressions are concerned, though, the option is transparent, so you need to deal only with the case name.

NOTE

If you’re following along in FSI, you’ll want to reset your session before proceeding with the next examples to avoid any potential naming conflicts between the active patterns.

To see partial active patterns in action, we can return to the FizzBuzz problem. Using partial active patterns lets us rewrite the solution more succinctly. We can start by defining the partial active patterns like this:

let (|Fizz|_|) n = if n % 3 = 0 then Some Fizz else None

let (|Buzz|_|) n = if n % 5 = 0 then Some Buzz else None

The first thing you probably thought after reading the preceding snippet is “Why are there only two cases when the problem specifically defines three?” The reason is that partial active patterns are evaluated independently. So, to meet the requirements, we can construct a match expression such that a single case matches both Fizz and Buzz with an AND pattern, as shown here:

let fizzBuzz =

function

| Fizz & Buzz -> "FizzBuzz"

| Fizz -> "Fizz"

| Buzz -> "Buzz"

| n -> n.ToString()

Now all that’s left is to print the required values just like we did before:

seq { 1..100 }

|> Seq.map fizzBuzz

|> Seq.iter (printfn "%s")

Parameterized Active Patterns

All of the active patterns we’ve seen so far have accepted only the single match value; we haven’t seen any that accept additional arguments that aid in matching. Remember, active recognizer functions are curried, so to include additional parameters in your active pattern definition you’ll need to list them before the match input argument.

It’s possible to construct yet another solution to the FizzBuzz problem using only a single Parameterized partial active pattern. Consider this definition:

let (|DivisibleBy|_|) d n = if n % d = 0 then Some DivisibleBy else None

This partial active pattern looks just like the Fizz and Buzz partial active patterns we defined in the previous section except that it includes the d parameter, which it uses in the expression. We can now use this pattern to resolve the correct word from any input, like so:

let fizzBuzz =

function

| DivisibleBy 3 & DivisibleBy 5 -> "FizzBuzz"

| DivisibleBy 3 -> "Fizz"

| DivisibleBy 5 -> "Buzz"

| n -> n.ToString()

Now, instead of specialized cases for Fizz and Buzz, we simply match whether the input is divisible by three or five through the parameterized pattern. Printing out the results is no different than before:

seq { 1..100 }

|> Seq.map fizzBuzz

|> Seq.iter (printfn "%s")

Summary

Pattern matching is one of F#’s most powerful and versatile features. Despite some superficial similarities to case-based branching structures in other languages, F#’s match expressions are a completely different animal. Not only does pattern matching offer an expressive way to match and decompose virtually any data type, but it even returns values as well.

In this chapter, you learned how to compose match expressions directly using match...with and indirectly using the function keyword. You also saw how the simple pattern types like the Wildcard, Variable, and Constant patterns can be used independently or in conjunction with other more complex patterns like those for records and lists. Finally, you saw how you can create your own custom patterns with complete and partial active patterns.