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.
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"
.
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
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 the List.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
, and Some 7
.
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 a MatchFailureException
:
> 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 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 make numberToString
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, 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"
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.
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).
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.
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"
When performing pattern matching against types where null
is a valid value, you’ll typically want to include a Null pattern to keep any null
s 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
to null
, 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())
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.
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.
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 with List.ofSeq
, Array.ofSeq
, or a comparable mechanism.
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 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.
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.
F# has two ways to match against particular data types: Type-Annotated patterns and Dynamic Type-Test 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 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"
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
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.
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 switch
statements.
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.
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.
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.
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 either Some(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.
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")
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")
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.