Introduction
Many times in programming we need to examine data structures
and their values, and test them against one or more conditions. When these
conditions are met, we perform certain computations, trigger transformations,
or take particular actions. To accomplish these types of operations in F#, you can
use a series of if…then…else statements;
however, pattern matching offers greater power and flexibility, and goes
well beyond more primitive if…then…else constructs.
Patterns
Before we can talk about pattern
matching, it makes sense to first define what patterns are. Patterns are rules for transforming data. We use patterns
throughout F# to do the following types of things:
·
compare data with a logical structure
·
decompose data into its constituent parts
·
extract information from constructs in various ways
Whereas other languages provide basic value-based decision
and branching mechanisms, patterns augment and improve upon this ability. They
provide the basis for making these same types of decisions based on the
structure of the data and its type we well. This ability enables you to
elegantly deconstruct and extract information from varying constructs in a
clean, efficient, and succinct manner – something that is difficult to do in
more traditional languages.
Pattern Matching
Pattern matching is the act of comparing a test expression
with one or more patterns. F# is quite flexible with respect to the types of
test expressions it’s capable of matching against. These include literals,
constants, single variables, multiple variables, tuples, array, lists, enumerations,
discriminated unions, records, and classes (to be covered) – and combinations
thereof. For the rest of this chapter, we’ll look at how pattern matching
works, and the types of things you can do with it.
Using match
The workhorse of pattern matching is the match
expression, which combines branching control with computation. The match
expression takes the following general form:
match test-expression with
| pattern1 [ when condition ] -> result-expression1
| pattern2 [ when condition ] -> result-expression2
| ...
Throughout this section, I will use the terms as specified
in the general form above. To be clear, let me explain the various components:
·
match expression. This is the entire construct, from the
word match to the end of the final branch (the code after the
vertical bars (|)).
·
test-expression. This is the input to the match
expression. It may be a numeric constant, character, a string, a manifest
constant, a simple variable, an array, etc. Most of F#’s types can be used as test-expressions.
·
pattern. This is the contour of symbols against which the test-expression
is evaluated. The pattern specifies how to try to deconstruct the test-expression.
·
when condition. This is a predicate that qualifies a
pattern match. In order for a match to be considered successful, the pattern
must satisfy the test-expression and the when
condition, if present (it’s optional) must evaluate to true.
·
result-expression. This is the expression that F# returns
when the corresponding pattern is considered to be a match for the test-expression
under consideration.
Pattern matching via the match expression
provide a “small language” to describe the structure and the types of values
you want to match against. I find it helpful to think of patterns as distant
cousins to regular expressions.
When the match expression executes, F# compares
the given test-expression with each of the specified patterns
in turn. When the test-expression matches a pattern,
the corresponding result-expression is evaluated and it returns a
value. This value is the result of the entire match expression.
Once a match is found, F# stops evaluating. This means that
you need to be aware how you order your patterns – you want to
put the most specific patterns earlier in the pattern
chain, and the more general ones towards the end; otherwise, the later ones will
never be tested. The entire match expression is limited to a single type,
meaning that all result-expressions must eventually resolve to
the same type.
Matching Anything - The Wildcard Pattern
The simplest of patterns is called the wildcard pattern.
The wildcard pattern is specified as a single underscore character (_)
and matches any test-expression. Thet
match 3 with
| _ -> printfn "wildcard
matches anything."
match "f#" with
| _ -> printfn "strings
work, too"
match
[1;2;3] with
| _ -> printfn "arrays work fine"
You can match the wildcard pattern againt all legal
test-expression types, and it will always yield a postive match.
Since the wildcard pattern “matches
anything”, it’s normally used as the very last pattern in match
expressions. It’s F#’s way of dealing with the fact that the test-expression
matched none of the other patterns. Using a
wildcard pattern prevents F# from needing to throw a MatchFailureException
when it exhausts its search for a positive match; instead, it will reach the
wildcard pattern and return the default expression.
Matching Constants
We can match constant test-expressions
against constant patterns. A constant pattern
only matches when it equals the test-expression.
// A constant pattern only matches when it equals the
test-expression
match 3 with
| 2 -> "Never
matches."
| 3 -> "OK.
This matches."
In this example, F# compared the test-expression 3 with the pattern 2, found no
match, and continued down the list of patterns in lexical order. When it found
the pattern
3, it
calculated a valid match, and evaluated the corresponding result-expression,
"OK. This
matches." The return type of the match
expression is string.
Although this example works fine, you should be aware that
if you compile or run it in the F# Interactive Console, F# will report the
following warning:
Program.fs(4,11): warning FS0025: Incomplete pattern
matches on this expression. For example, the value '0' may indicate a case not
covered by the pattern(s).
This means that F# has detected that it’s possible for a
caller to pass in a test-expression that does not have match any of
the patterns
specified. We can handle this by using the wildcard pattern as a catch-all
pattern:
match 3 with
| 2 -> "Never
matches."
| 3 -> "OK.
This matches."
| _ -> "Catch-all.
Will never get here."
Overall, this isn’t a very compelling example, and if all
you could do was pattern match over literals, pattern matching wouldn’t be very
useful. Let’s look at how symbols and manifest constants are used in
conjunction with matching.
Matching Manifest Constants
In the following code, we define 2 manifest constants, GOLDENRATIO
and LOG10.
We then want to write match expression that uses these constants as patterns.
[<Literal>]
let
GOLDENRATION = 1.618
[<Literal>]
let
LOG10 = 2.303
let foo
x =
match x with
| GOLDENRATION -> "The Golden Ratio"
| LOG10 -> "Log
10 is Used Everywhere"
When you want to match against a manifest constant, you need
to tell F# that the pattern is actually a literal and not a symbol
pattern. You do this using the LiteralAttribute. If
you fail to specify [<Literal>], the pattern matching engine
will match the test-expression against any symbol and will
always match the first pattern it finds. Here is the same example without the
benefit of the [<Literal>] attribute.
let
GOLDENRATION = 1.618
let
LOG10 = 2.303
let foo
x =
match x with
| GOLDENRATION -> "The
Golden Ratio"
| LOG10 -> "Log
10 is Used Everywhere"
let result = foo 1.23
When run in the F# Interactive Console, we get the following
result:
val GOLDENRATION : float = 1.618
val LOG10 : float = 2.303
val foo : 'a -> string
val result : string = "The Golden Ratio"
The test-expression 1.23 has somehow matched the GOLDENRATIO
pattern,
even though the values are not equal. What happened? The pattern matching
engine didn’t know it was supposed to match 1.23 against the value 1.618; therefore,
it treated GOLDENRATIO as a new unbound local symbol which can bind to anything.
In fact, the expression let result = foo "abc"
would have worked, too! This is probably not the way we want the
match to behave.
The moral of the story is when your patterns
are manifest constants, make sure to have attributed these constants with [<Literal>].
When used in this way, pattern matching is akin to a basic switch
statement in C-based languages.
Just to show pattern matching using different data types, let’s
look at another example that uses strings for the test-expression and patterns.
// Patterns and test expressions can be strings, too.
match "hello" with
| "goodbye" -> "Never matches."
| "hello" ->
"Always matches."
| _ -> "We'll
never get here."
Matching Symbols
Patterns can be symbols as well. Symbols match
the “shape” of the test-expression data. In the following example,
the pattern
x
will always match the test-expression 3, since the single
value 3 can be positively bound to the pattern x
– they “match up” in structure.
// Patterns always match with a symbol that's not an enum, DU, or
literal constant
match 3 with
| x -> "This
always matches here."
| 3 -> "This
would match, but we never get here!"
| _ -> "This
would match, but we never get here either!"
Value Binding
When comparing a test-expression against
a pattern,
F# automatically assigns the test-expression’s
value(s) to the symbol(s) specified in the associated pattern.
For example, when we pattern match against 3 and x
above, x is initialized with the values from the test-expression,
3.
We call this automatic value-to-symbol association value
binding. In general, value binding is a powerful by-product of matching because
it enables the subsequent use of the values in downstream calculations. This
economy of expression makes for succinct, expressive code. It is used quite
naturally in many pattern matching scenarios.
When symbols are used as patterns, they can be
leveraged in result-expression calculations and as return
values:
match 3 with
| x -> x + 2 //
returns 5
| 3 -> 6 //
we'll never get here, and never return 6
| _ -> 7 // we'll never get here, and never return 7
match expressions can act surprisingly when
symbol patterns use the same name as other values that are in scope. Let’ look
as a few examples:
// Local value x
let x = 5
match 3 with
| x -> x // returns
5!
// Returns 5, because x is not a local pattern symbol. y is, so y
is bound to 3,
// but not subsequently used. We might as well have used a wildcard instead of
y
// here.
Qualifying Matches Using When Guards
F# enables us to augment patterns with
conditions called when guards. When guards are Boolean checks,
a.k.a., predicates, which allow us to qualify patterns. When
guards are used in conjunction with patterns to constrain
what constitutes positive matches. The following example demonstrates their
use:
let sign
x =
match x with
| x when x < 0 ->
-1 // match only when x < 0
| x when x = 0 ->
0 // match only when x = 0
| x when x > 0 ->
1 // match only when x > 0
| _ -> int nan // nan = "not a number" from System.Double.NaN
With when guards, the wildcard pattern
is sometimes used so that the guts of the pattern match is based
on the guard condition. It is not uncommon, for example, to see a function like
sign
(above) implemented like this:
let sign
x =
match x with
| _ when x < 0 ->
-1 // match only when x < 0
| _ when x = 0 ->
0 // match only when x = 0
| _ when x > 0 ->
1 // match only when x > 0
| _ -> int nan // nan = "not a number" from System.Double.NaN
Here, we use the wildcard pattern to match
whatever test-expression is passed into the match
expression. We then base a positive match on the when guard’s return
value.
We can leverage when guards as part of any pattern
test. In the following example, we use a local binding in conjunction with a when clause.
Note that the when
clauses uses the bound value of x, not the externally defined x:
let x = 5
match 3 with
| x when x = 5 ->
6 // here x=3, not 5, so we fail the when clause
| _ -> x // this
match succeeds and returns 5 for the match expression
Matching Patterns and Types
Patterns are not limited to single literals or variables. We
can use patterns to match against tuples, list constructs, arrays, and various
user-defined types. The following examples demonstrate using pattern matching
with the types we’ve covered so far.
Matching Multiple Variables
Our test-expressions can contain more than one
variable, as can the patterns we match against. In the following
example, our test-expression contains two variables – an int
and a string – and returns a Boolean.
let
allowAccess userid pwd =
match userid, pwd with
| 123, "123" -> true
| 505, "xyz" -> true
| _, "**9" -> true
| 411, _ -> true
| _ -> false
Matching Tuples
F#’s pattern matching engine is also capable of matching
against tuples. The following function accepts a tuple of type bool *
bool and uses the tuple as its test-expression.
let
orTable(b1, b2) =
match(b1, b2) with
| (true, true) -> true
| (true, false)
-> true
| (false, true)
-> true
| (false, false) -> false
In this example, we did not need to provide a default
branch, since we’ve covered all possible pattern combinations. F# recognizes
that our patterns are logically complete, so does not issue a warning or an
error.
Matching Arrays
When we match using an array pattern, in order to match
successfully, the test-expression must be an array of the same
size and makeup as the pattern being evaluated, and must contain the
elements in the same order as the candidate pattern.
let
digitmsg a =
match a with
| [|0;1|] -> "binary
digits"
| [|2;4;6|] -> "some
even digits"
| _ -> "who knows?"
If we were to call digitmsg with [|0; 1|],
we’d get a postive match, whereas calling it with [|1;0|] would result in
a non-match.
The patterns used with the match expression must
all be of the same type. For example, the following produces a type-based
error:
// Match array (error)
let
digitmsg a =
match a with
| [|2; 4; 6|] -> "some
even digits"
| [|"test"|] -> "does it
work?" // error: used string,
expected int
| _ -> "who knows?"
Note that we can use the wildcard pattern to match
individual array elements:
let
flower a =
match a with
| [|"roses"; _|] -> "roses are
red"
| [|_; "violets"|] -> "violets are
blue"
| [|_; _|] -> "this matches any two things"
| _ -> "catch-all"
Matching Enums
You can use pattern matching to match against enumerations.
The enumeration pattern must use the entire qualified name:
type
Protocol =
| UDP = 0 | TCP = 1
let
sendPacket proto packet =
match proto with
| Protocol.UDP -> "sending
UPD"
| Protocol.TCP -> "sending
TCP"
| _ -> "unknown protocol"
Matching Lists
F# also supports working with list-based patterns. The
lists can be processed using literals, wildcards, and recursion:
// Lists can be matched as literals
match [1;2;3] with
| [1;2;4] -> "doesn't
match"
| [3;2;1] -> "doesn't
match, order counts"
| [1;2;3] -> "OK
- a match"
| _ -> "should
never get here"
// The contents of the list can be any valid type, e.g., strings
or tuples.
// Remember that all list elements must be of the same type, e.g., all strings.
let
famousDuo pair =
match pair with
| ["bonnie"; "clyde"] ->
true
| ["adam"; "eve"]
-> true
| ["ozzie"; "harriet"] -> true
| ["gilbert"; "sullivan"] ->
true
| _ -> false
let
isFamous = famousDuo ["adam"; "eve"]
// order matters. ["eve";
"adam"] would return false.
Lists can be matched with missing literals, by using a wildcard
match
[1;2;3] with
| [1;_;4] -> "doesn't
match in the last element 3 vs. 4"
| [1;2;_] -> "OK
- the underscore matches to 3"
| [_;2;3] -> "we'll
never get here, but this would match if we did"
| _ -> "should
never get here"
// Symbols can be used in patterns to match part of a list
match
[1;2;3] with
| [1;x;4] -> "Doesn't
match in the last element 3 vs. 4"
| [1;2;x] -> "OK
- the x is bound to 3 - in this line only!"
| [x;2;3] -> "We'll
never get here, but this would match if we did"
| _ -> "Should
never get here"
As you’ll recall, when we process lists, we normally do so
recursively. To support recursive pattern matching on lists, F# supports two additional
patterns:
·
[]. This is the empy-list pattern. It used to
test a list to see if it’s empty.
·
pat::pat. This is the cons pattern and is used
to match the head and tail of the list.
In the following example, we create a recursive function
that counts the number of elements in the given list. Although contrived, this
example helps demonstate the empty-list and cons patterns:
let rec count lst =
match lst with
| [] -> 0 // empty list -> 0 elements
| [_] -> 1 // any one thing -> 1 element
| [_;_] -> 2 // any two things -> 2 elements
| _ :: t -> count t + 1 // cons pattern. count recursively.
Let’s take a look at another list-based example that uses
the cons pattern to sum up all the numbers in the collection:
let rec sum theList =
match theList with
| [] -> 0
| h :: t -> h + sum t
let theSum = sum [0..2..100] //
2550
Using pattern matching in conjunction with recursive list
processing is a common idiom in F# programming.
Matching Records
The F# pattern matching engine also recognizes records. To
work with pattern matching records, you can match on individual record fields
or combinations thereof. In the following example, we define a record type Employee
and a function calcRaise. We then use various combinations of
patterns to calculate raises based on a various criteria.
type
Employee = {
rank: string;
name: string;
salary: float
}
let
calcRaise emp =
match emp with
| { rank="manager"; name="Smith" } ->
emp.salary * 1.10
| { rank="manager"} when emp.salary <= 75000.0 -> emp.salary * 1.20
| { rank="indi"} -> emp.salary * 1.30
| _ -> 0.0
Matching Discriminated Unions
Last but not least, let’s take a look at using pattern
matching in conjunction with DUs. In this next example, we demonstrate how to
match against a DU’s discriminator using a variety of techniques. We also
demonstrate a somewhat common idiom of nesting match expressions:
type
Weapon =
| Knife of int | Sword of
int | Mace of int * bool
let k =
Knife(100)
let s =
Sword(500)
let m =
Mace(750, true) //
true=is magic
let
showDamage w =
match w with
| Knife(damage) -> printfn "knife caused %d damage points" damage
| Sword(_) -> printfn "sword caused 1000 damage points"
| Mace(damage, isMagic) ->
match isMagic with
| true ->
printfn "mace caused %d damage points"
(damage * 5)
| false ->
printfn "mace causes %d damage points"
damage
showDamage
k
showDamage
s
showDamage m
The id Pattern
F# also supports the id pattern, which is a mechanism
for explicitly invoking value binding. In the following example, we use the id
pattern to make a copy of a list, binding the list’s values to new identifiers,
by position:
let lst
= [1; 2; 3]
let [x;
y; z] as lst2 = lst
The id pattern is actually used with let
– and not match. It also includes using the keyword as,
as shown above. You can think of the id pattern as being invoked via let…as.
After executing the above example, x = 1, y = 2
and z
= 3. If the lists were of different sizes, F# would have
generated a compile error.
Pattern Groups
In addition to being able to perform single and qualified
matches, e.g., with when guards, we can also group matchues using “or” and
“and” patterns. First, let’s look at “or” patterns, which allow us to chain
together alternative patterns and pick among them. The “or” pattern uses the
verical bar (|) to separate its alternatives. The following
example uses an “or” pattern to match countries with currencies:
// Matching one of several alternatives with "or"
let
currencyForCountry (country: string) =
match country.ToLower() with
| "us" | "east
timor" | "ecuador" | "panama" ->
"U.S. Dollar"
| "portugal" | "spain" | "austria"
| "belgium" -> "Euro"
| _ -> "unknown currency"
In this example, we explicitly declare the type of the argument country
to be string. We need to do this so that the call to country.ToLower()
compiles correctly; otherwise, the F# compiler complains that it
cannot fully determine country’s type, and thus cannot guarantee that
the ToLower
function exists.
In addition, when we explicitly declare function argument
types, as in this example, we must surround the arguments and their types with
parens to ensure F# evaluates them properly. If we omit the parens, F#’s order
of evaluation rules cause it to consider country a generic vs.
its assigned type.
To complement the “or” pattern, F# offers the “and” pattern.
The “and” pattern requires that the test expression matches two or more
separate patterns. The types on both sides of the “and” pattern must be
compatible. The “and” patterns uses the ampersand (&) symbol to join
the associated patterns. The following example was taken from the MSDN
documentation on patterns:
let
detectZeroAND point =
match point with
| (0, 0) -> printfn "Both
values zero."
| (var1, var2) & (0, _) -> printfn "First value is 0 in (%d, %d)" var1 var2
| (var1, var2) & (_, 0) -> printfn "Second value is 0 in (%d, %d)" var1 var2
| _ -> printfn "Both
nonzero."
We can then call the detectZeroAND function
like so:
detectZeroAND
(0, 0)
detectZeroAND
(1, 0)
detectZeroAND
(0, 10)
detectZeroAND (10, 15)
Alternative Functional Representation of match
F# offers another, shorthand form of pattern matching syntax
– and it does not use the match keyword. Instead,
this form of pattern matching uses the keyword function to introduce a
lambda expression (the “pattern matching function”) in which the matching is
performed immediately on the lambda’s argument.
In the following example, we create a pattern matching
function that accepts a string representing a meal, and returns the
corresponding price as a floating point value:
let
menuPrice = function // implicit un-named parameter
| "breakfast" -> 3.49
| "lunch" -> 8.75
| "dinner" -> 18.50
| "drink" -> 2.50
| _ -> 0.0
let cost = menuPrice "lunch"
+ menuPrice "drink"
Note that the function’s argument is implicit and its type is
inferred by the type of the patterns (a string in this example). Because
the argument is implicit, there can be only one.
If we want to use the lambda pattern matching syntax, and still
need to pass multiple values into our test-expression, we can
use tuples or other aggregate data structures like discriminated unions. In the
following example, we use a tuple to represent a point in some arbitrary square
whose upper-left point is (0,0) and whose lower right point is (1.0,
1.0).
let
isPointInSquare = function
| (0.0, 0.0) -> false
| (1.0, 1.0) -> false
| (a, b) when a > 0.0 && a < 1.0
&& b > 0.0 && b < 1.0 ->
true
| _ -> false
As demonstrated here, we have access to the implied lambda
argument. We can see this clearly via the (a, b) pattern
in this example. When we specify the pattern (a, b),
value binding maps the lambda’s input values to the labels a
and b
respectivley.
Active Patterns
With the pattern matching mechanisms we’ve discussed so far,
the test-expressions
and the patterns have been completely static, i.e., they are fixed
at compile time and do not change during the execution of the program.
What if we’d like to make our patterns “come alive”
so that we could programmatically control and change them during the process of
matching? Well, we can via active patterns. The fact that these patterns
can change under program control is what makes them “active.”
Simple Transformations
The simplest form of an active pattern acts as a
transformation. It accepts a single argument as input and transforms it in some
way. The transformed value is the pattern used in the
match expression. In the following example, we use an active pattern that
converts its input, assumed to be a temperature in Fahrenheit, to the
equivalent Celsius measure:
let
(|F2C|) f =
(f - 32.0) * (5.0 / 9.0)
This active pattern looks exactly like a regular function.
The only difference is how the function’s name is bracketed by a set of
vertical bars, colloquially called “banana clips.” The banana clips tell the F#
compiler that we intend to use this function (active pattern) in conjunction
with pattern matching, and that it’s not a general purpose function to be
called arbitrarily. We need active patterns to implement this type of behavior
because regular functions cannot be used as pattern
candidates.
So, active patterns are like functions called in the context
of pattern matching. We place them lexically where we normally place static
patterns; however, when F# sees an active pattern vs. a static pattern, it
invokes the active patternand uses the function’s result as the pattern
value.
We can see how to use our F2C active pattern in
the following example:
let test
(f: float) =
match f with
| F2C c when c > 30.0 -> printfn "it's
hot %f" c
| F2C c when c > 20.0 -> printfn "it's
temperate %f" c
| F2C c when c > 10.0 -> printfn "it's
cool %f" c
| _ -> printfn "it's getting cold!"
Notice here we’re using our active pattern F2C
where we’d use a normal, static pattern. The variable f
is used as the input to active pattern, and c is the output. To
process the active pattern, the match expression compares the value of f
with the result of calling F2C c, and prints out a
corresponding message.
Arbitrary Decomposition
In addition to converting single values, we can use active
patterns to decompose most any data structure in a customized manner. To
perform custom decomposition, we define “named partitions” that subdivide the
data using criteria that we define. We can then use these named partitions in
subsequent matching clauses. Active patterns come in two flavors: complete
active patterns and partial active patterns. We will look at each
one in turn.
Complete Active Patterns
While it is very convenient to think of active patterns as
functions, there’s one wrinkle. Whereas functions have a single name, active
patterns can have multiple names, or more accurately, multiple identifiers.
Active patterns with single names, a.k.a., single identifiers, such as F2C
above, are more formally known as single-case active patterns. In the
general case, active patterns can include more than one identifier and are
known as multi-case active patterns. Single-case and multi-case active
patterns that use all of their inputs are called complete active patterns.
Let’s take a look at the following example, where we define
a complete, multi-case active pattern:
let
(|Child|Teen|Adult|Senior|) age =
if age < 13 then
Child
elif age >= 13 && age < 18 then Teen
elif age > 18 && age < 60 then Adult
else Senior
This active pattern includes 4 identifiers: Child,
Teen,
Adult,
and Senior.
It is often helpful to think of these as “buckets” or partitions into which
we’ll assign inputs. We also define an input argument, age,
that we use as the basis of the partitioning. In the following example, we use
this active pattern to process a list of people’s ages:
let rec showPeople p =
let mutable
tail = []
match p with
| [] -> printfn "<done>"
| h :: t ->
printf "%d = " h
tail <- t
match h with
| Child -> printfn "child";
| Teen -> printfn "teen";
| Adult -> printfn "adult";
| Senior -> printfn "senior";
showPeople tail
We can now test the showPeople function as
follows:
showPeople [4; 10; 42; 15; 34; 19; 66; 48; 55; 32;
21; 18; 81; 74; 65; 14]
The thing that makes these patterns “active” is the fact
that a function executes in order to partition the input.
Note that an active pattern identifier must begin with an
uppercase character. This is one of the few places where F# dictates case
outside of its keywords.
Partial Active Patterns (Some and None)
Although it may sounds complex, a partial active pattern
is a special case of a single-case active pattern that returns an option
type, i.e., it either returns Some or None.
A partial active pattern is characterized by the fact that it returns an option
and F#’s pattern matching mechanisms recognize that a Some
characterizes a valid match, while a None does not ;
otherwise, it behaves exactly like the active patterns we’ve discussed already.
T o define a partial active pattern, you append a wildcard
character (_) to the end of the identifier inside the
banana clips. The following example, taken from an article by
Pickering, demonstrates a partial active pattern used to match different
date formats:
open
System
let
invar = Globalization.CultureInfo.InvariantCulture
let
style = Globalization.DateTimeStyles.None
let
(|ParseIsoDate|_|) str =
let res, date = DateTime.TryParseExact(str, "yyyy-MM-dd", invar, style)
if res then
Some date else None
let
(|ParseAmericanDate|_|) str =
let res, date = DateTime.TryParseExact(str, "MM-dd-yyyy", invar, style)
if res then
Some date else None
let
(|Parse3LetterMonthDate|_|) str =
let res, date = DateTime.TryParseExact(str, "MMM-dd-yyyy", invar, style)
if res then
Some date else None
let
parseDate str =
match str with
| ParseIsoDate d -> d
| ParseAmericanDate d -> d
| Parse3LetterMonthDate d -> d
| _ -> failwith "bad
date" // throws exception (covered
later)
let d1 =
parseDate "05-23-1978"
let d2 =
parseDate "May-23-1978"
let d3 =
parseDate "1978-05-23"
let d4 = parseDate "05-23-78"
// shoud fail
In this example, we use partial active patterns to parse a
given date string. Since the string may be in one of several formats, we define
several active patterns, one per date format.
Active patterns are also useful when implementing “and” patterns.
Recall from Chapter 11 that “and” patterns consist of multiple match clauses.
To be considered a successful match, the input must satisfy all of the
clauses. In the following example, we use active patterns to succinctly
implement an “and” pattern to ensure that a given number is a both a multiple
of 3 and is even:
let
(|MultipleOf3|_|) n = if n % 3 = 0 then Some(n) else
None
let
(|Even|_|) n = if n % 2 = 0 then Some(n) else
None
let
checkNum x =
match x with
| MultipleOf3 a & Even b -> printfn "%d is a multiple of 3 and is even." x
| _ -> printfn "%d is just %d" x x
A couple of important things to note. First, remember that every
active pattern receives at least one input – the test-expression in the
match expression. In the checkNum example above, the test-expression
is x.
Second, when a pattern returns a value, we capture it in an output variable. This
is the purpose of the a and the b in the checkNum
function above. If we omit a, for example, the compiler will
complain that we have an expression of type unit being used with an
expression of type int. In other words, the pattern is returning a
value (the int) and we’ve no place to put it.
The placement of active pattern inputs and output can be a
little bit confusing. We’re used to specifiying input arguments explicitly and
placing them after the name of the function. In the case of active patterns,
the input is implicit via the test-expression, and the variable following the
active pattern name is actually the output (the result of the active pattern
executing).
Parameterized Active Patterns
Active patterns accept at least one input argument – the test-expression
a.k.a., the data being matched via the match expression. Active patterns can
accept additional arguments as well, in which case they are called parameterized
active patterns. These additional arguments allow us to constrain and/or
specialize the processing our active patterns performs. In the following
example, we define an active pattern that enables us to parse a substring from
an input string:
let
(|StringMatches|_|) (mytarget : string) (mystring : string) =
if (mystring.Contains(mytarget)) then Some(true) else None
We can use this active pattern as follows:
let
stringContains str target =
match str with
| StringMatches target result -> printfn "%s contains %s" str target
| _ -> printfn "Substring not found"
Here again, I’d like to emphasize the somewhat confusing
placement and binding of active pattern arguments. In this example, the test-expression
str
serves as input to the StringMatches active pattern. The match’s test-expression
always binds to the last (right-most) parameter in the the active
pattern. This means that in this example, str binds to the active
pattern’s mystring argument. In addition, the target
argument from the match expression binds to the mytarget argument in
the active pattern, and the result variable binds to the active
pattern’s Some/None return value.
What You Need to Know
·
Patterns are rules for mapping and transforming input
data.
·
F# uses pattern matching to map patterns to values.
·
The match expression is used to invoke pattern
matching. The match expression is like a set of if…then…else
or switch
statements, but more powerful.
·
You use the wildcard pattern (_) to match “anything.”
·
With the match expression, you can match constants,
single values, multiple values, tuples, arrays, lists, and cons
(::).
You can also match records, discriminated unions, object types/constraints, and
nulls.
·
You can combine match expressions with
“or” and “and” constructs.
·
You can use when guards to augment patterns with
predicates.
·
There is an alternative form of the match expression that
uses a lambda via the function keyword. This lambda accepts a single,
implicit argument available to the body of the lambda.
·
Active patterns enable us to define a function that F# calls when
pattern matching. The result of the function is used as the pattern F# uses to
match against the test expression.
|