Chapter 6. Pattern Matching

WHAT'S IN THIS CHAPTER?

  • Understanding patterns and pattern-matching

  • Using pattern matching types

  • Applying pattern guards

  • Using active patterns

More so than any other construct thus far explored, pattern matching is what distinguishes F# from the other languages in the .NET family. Pattern-matching is a hallmark of the functional language, and its power is something that is rapidly finding its way (in various guises) into other languages.

BASICS

Fundamentally, pattern-matching looks, on the surface, like a variation on the switch/case construct from the C-family of languages: A value is tested, and depending on its contents, one of several different "branches" of code is evaluated:

let x = 12
match x with
| 12 -> System.Console.WriteLine("It's 12")
| _ -> System.Console.WriteLine("It's not 12")

The syntax is somewhat similar to the switch/case of C#; broken down, a pattern-match consists of the following:

  • The match keyword, preceding the expression to be evaluated

  • The with keyword, indicating the start of one or more various values to compare against

  • The vertical pipe character (|) at the start of each match clause to evaluate against

  • A match clause, which can take one of several different forms as described later in this chapter

  • The arrow (->), separating the match clause from the expression to execute if the match clause succeeds

As is consistent with F#'s syntax, the underscore (_) character acts as a wildcard when used: anything that doesn't match against preceding clauses will match against this one.

Note that unlike the switch/case from imperative languages, the pattern-match is an expression, meaning that it, too, yields a value when evaluated:

let y = match x with
        | 12 -> 24
        | _ -> 36

This means that the various clauses in the pattern-match must all yield compatibly typed values, just as with the if/then construct (see Chapter 4 for details).

Leaving the discussion there, however, you miss out on all the other fun things that pattern-matching can do, such as value extraction into local variable bindings:

let people = [
   ("Ted", "Neward", 38)
   ("Mark", "Richards", 45)
   ("Naomi", "Wilson", 38)
   ("Ken", "Sipe", 43)
]
List.iter
    (fun (p) ->
        match p with
        | (fn, ln, a) ->
            System.Console.WriteLine("{0} {1}", fn, ln)
        | _ ->
            failwith "Unexpected value"
    )
    people
                                                         
BASICS

In the preceding code, the List.iter function executes the anonymous function against each of the string/string/int tuples in the list. (For more on lists and tuples, see Chapter 5; for more on anonymous functions, see Chapter 13.) The important part of this example is the first match clause: If the value matched is a three-part tuple — which F# infers because the match clause uses a three-part tuple syntax ((fn, ln, a)) — then the individual elements of the tuple are each bound to the local variables fn, ln, and a, respectively. Because the previous example never actually uses the third part of the tuple, we can again leverage the wildcard pattern to indicate that it is irrelevant to the remainder of the expression:

let people = [
    ("Ted", "Neward", 38)
    ("Mark", "Richards", 45)
    ("Naomi", "Wilson", 38)
    ("Ken", "Sipe", 43)
]
    List.iter
        (fun (p) ->
            match p with
            | (fn, ln, _) ->
                System.Console.WriteLine("{0} {1}", fn, ln)
            | _ ->
                failwith "Unexpected value"
        )
        people
                                                              
BASICS

The match expression itself doesn't just have to be a variable, and frequently serves as a way to make values easier to match inside of match clauses:

let p = new Person("Ken", "Sipe", 45)
let lastName = match (p.FirstName, p.LastName, p.Age) with
                | ("Ken", "Sipe", _) -> p.LastName
                | _ -> ""

Note

Many of the examples in this chapter reference the Person class, which is defined in Chapter 8.

In this case, the match expression is a three-part tuple, built from the three properties of the Person type (FirstName, LastName, and Age). Thus, only if p.FirstName matches against the constant value "Ken", p.LastName matches against "Sipe", and p.Age matches against any value (because of the use of the wildcard _ here) will the expression to the right side of the arrow be evaluated. If the match isn't made, the second match clause, the wildcard expression, will be matched, yielding an empty string.

Just because Person has three properties on it doesn't mean we have to use all those properties; because we don't care about the age of the Person p, the above expression could have been written more simply as:

let p = new Person("Ken", "Sipe", 45)
let lastName = match (p.FirstName, p.LastName) with
               | ("Ken", "Sipe") -> p.LastName
               | _ -> ""

Again, the match is defining a tuple out of p.FirstName and p.LastName and then matching it against possible expression types and values to find a block of code to execute.

This capability to match a variety of possible types and values and bind those values into local variables for later use makes pattern-matching a valuable construct for performing data-manipulation tasks against a collection, such as a list:

let persons = [
    new Person("Ted", "Neward", 38)
    new Person("Ken", "Sipe", 43)
    new Person("Michael", "Neward", 16)
    new Person("Matthew", "Neward", 9)
new Person("Mark", "Richards", 45)
        new Person("Naomi", "Wilson", 38)
        new Person("Amanda", "Sipe", 18)
        ]
    List.iter
        (fun (p : Person) ->
            match (p.FirstName, p.LastName) with
            | (fn, "Sipe") ->
                System.Console.WriteLine("Hello, {0}!", fn)
            | (fn, "Neward") ->
                System.Console.WriteLine("Go away, {0}!", fn)
            | _ ->
                System.Console.WriteLine("Who the heck are you?")
        )
        persons
                                                            
BASICS

Using pattern-matching to sift through a collection of data (such as the preceding Persons list) is a common idiom in F#, as is pattern-matching using the Option type to avoid null dereferences (and the subsequent exception) when searching through a collection of data. Of all the syntactic constructs in the F# language, pattern-matching is likely the most valuable — and thus the most important — to learn.

PATTERN TYPES

A variety of different match constructs are available for use in the body of the match expression. Note that any of these can be combined with any other of the pattern types, allowing F# developers to "mix and match" as the mood suits them. This can sometimes create some surprising effects and can potentially lead to some surprising results — remember that matches are evaluated in a top-down fashion, and to help out, wherever possible the F# compiler will assist as best it can in finding matches that will never allow any further match to succeed, or when the pattern-match leaves some particular value or range of values out. The compiler isn't perfect, however, and when it detects an unmatched construct at runtime, a MatchFailure exception will be generated and thrown.

Constant Patterns

As already demonstrated, a pattern-match can match against constant values in a manner entirely similar to that of the switch/case construct from any C-family language:

let x = (new System.Random()).Next(5)
let message = match x with
               | 0 -> "zero"
               | 1 -> "one"
               | 2 -> "two"
               | 3 -> "three"
               | 4 -> "four"
               | 5 -> "five"
               | _ -> "Unknown: " + x.ToString()
                                                          
Constant Patterns

As might well be expected, the match only works if the value stored in x is equal to the value in an individual match clause, such that if x holds the value 3, the corresponding result for message will be "three". Officially, this evaluation of equality is done using the F# method FSharp.Core .Operators.(=) (defining operator methods is described in more detail in Chapter 8).

Note that null is an acceptable constant value to match against, though F# code in general frowns on the use of null, preferring to use Option types (see Chapter 5) instead, using None to represent no value.

Unlike the switch/case, multiple constants can be matched, allowing easy construction of state machines or truth tables:

let x = (new System.Random()).Next(2)
let y = (new System.Random()).Next(2)
let quadrant = match x, y with
                | 0, 0 -> "(0,0)"
                | 0, 1 -> "(0,1)"
                | 0, 2 -> "(0,2)"
                | 1, 0 -> "(1,0)"
                | 1, 1 -> "(1,1)"
                | 1, 2 -> "(1,2)"
                | 2, 0 -> "(2,0)"
                | 2, 1 -> "(2,1)"
                | 2, 2 -> "(2,2)"
System.Console.WriteLine("We got {0}", quadrant)
                                                     
Constant Patterns

In the preceding code, the F# compiler will emit a warning about possible unmatched cases, because it cannot determine that the ranges of x and y are limited to 0 and 2. This is F# trying to help avoid a runtime exception, because it can't determine that the values of x and y will be limited to the range 0 to 2 (as defined by the Random.Next() method call).

Variable-Binding ("Named") Patterns

If the match clause consists of a name beginning with a lowercase letter, then it is a variable-binding pattern, and it introduces a new local variable into which the value is bound for use in the expression to the right of the arrow:

let p = new Person("Rachel", "Reese", 25)
let message = match p.FirstName with
               | fn -> "Hello, " + fn
System.Console.WriteLine("We got {0}", message)

Admittedly, this particular example is a bit contrived, because we could just as easily have obtained said value by simply using the FirstName property on the Person object, but this will become much more useful when combined with some of the other patterns, such as tuple or list patterns. When combined with constant patterns, it allows for a useful "default" case:

let p = new Person("Rachel", "Appel", 25)
let message = match p.FirstName with
                | "Rachel" -> "It's one of the Rachii!!"
                | fn -> "Alas, you are not a Rachii, " + fn
System.Console.WriteLine("We got {0}", message)

Variables bound this way are scoped to the match construct itself.

AND, OR Patterns

Patterns can combine using the Boolean operator "|" (for "or"), so that the right expression will be evaluated if either of the two clauses is a match:

let p = new Person("Rachel", "Reese", 25)
let message = match p.FirstName with
                | ("Rachel" | "Scott") ->
                    "Hello, " + p.FirstName
                | _ ->
                    "Who are you, again?"
System.Console.WriteLine("We got {0}", message)

Patterns can also use the & to connect two clauses that must both match for the right expression to fire, but for the most part, this will be limited to the use of active patterns discussed next.

Literal Patterns

Given what's been said already about F#'s binding of values to names and the pattern-matching rules, it would seem reasonable to expect the following to work:

let rachel = "Rachel"
let p = new Person("Rachel", "Reese", 25)
let message = match p.FirstName with
                | rachel -> "Howdy, Rachel!"
System.Console.WriteLine("We got {0}", message)

And, it will — it will work too well as the compiler will tell us when we try to add any additional clauses to the pattern-match:

let Rachel = "Rachel"
let p = new Person("Rachel", "Reese", 25)
let message = match p.FirstName with
                | Rachel -> "Howdy, Rachel!"
                | _ -> "Howdy, whoever you are!"
System.Console.WriteLine("We got {0}", message)

The F# compiler will emit a warning that the wildcard clause will never be matched, which seems strange. However, a little bit further investigation reveals that despite the direct literal assignment, F# doesn't consider rachel to be a constant to compare against. Instead, it introduces a new binding; to be precise, F# binds the value of the match (p.FirstName) into a new name, rachel, for the duration of the pattern-match.

To tell F# that the binding "rachel" should be treated as a literal value in the pattern match, the .NET attribute "Literal" (introduced in Chapter 3) should be put on the let expression that introduces it:

[<Literal>]
let Rachel = "Rachel"
let p = new Person("Rachel", "Reese", 25)
let message = match p.FirstName with
                | Rachel -> "Howdy, Rachel!"
                | _ -> "Howdy, whoever you are!"
System.Console.WriteLine("We got {0}", message)

Now the warning goes away, and the pattern-match behaves as expected.

Note

Notice that in order for the language to recognize the Literal as an actual literal, it must be uppercased; leaving the Literal identifier written as "rachel" will still fire the warning.

Tuple Patterns

Technically, we've already seen tuple matching; in the constant patterns section, we showed the following:

let x = (new System.Random()).Next(2)
let y = (new System.Random()).Next(2)
let quadrant = match x, y with
                | 0, 0 -> "(0,0)"
                | 0, 1 -> "(0,1)"
                | 0, 2 -> "(0,2)"
                | 1, 0 -> "(1,0)"
                | 1, 1 -> "(1,1)"
                | 1, 2 -> "(1,2)"
                | 2, 0 -> "(2,0)"
                | 2, 1 -> "(2,1)"
                | 2, 2 -> "(2,2)"
System.Console.WriteLine("We got {0}", message)
                                                           
Tuple Patterns

What we didn't say at the time is that the F# language considers the x, y pair to be a tuple. F# allows tuples to be used both in the match value and the criteria, as the preceding code demonstrates. Tuple-based pattern-matching combined with named patterns is one of the most common ways to extract the values out of a tuple:

let t = ("Aaron", "Erickson", 35)
let message = match t with
                | (first, last, age) ->
                    "Howdy " + first + " " + last +
                    ", I'm glad to hear you're " +
                    age.ToString() + "!"
System.Console.WriteLine("We got {0}", message)

Tuples and pattern-matching go well together, so much so that it should be assumed that wherever tuples are used, pattern-matching will be used to extract and manipulate the data from inside the tuple.

as Patterns

The as keyword allows us to do both extraction from a tuple and bind a new name to the original tuple at the same time:

let message = match t1 with
                | (x,y) as t2 ->
                     x.ToString() + " " +
                     y.ToString() + " " +
                     t2.ToString()
System.Console.WriteLine("We got {0}", message)

In this case, the original tuple value (t1) is not only extracted into the individual values x1 and y1, but also into the tuple value t2. This pattern-match type isn't common, at least not in this form; see the section "Beyond the Match" for details.

List patterns

Pattern-matching can also be used to match and extract values from lists, usually by extracting the head and the tail of the list into local values:

let numbers = [1; 2; 3; 4; 5]
let rec sumList ns = match ns with
                        | [] -> 0
                        | head :: tail -> head + sumList tail
let sum = sumList numbers
System.Console.WriteLine("Sum of numbers = {0}", sum)

Here the list is matched either against the empty list, or the head of the list is extracted into the local value "head" and the tail (which may be empty, if the incoming list is exactly one element long) is extracted into the local value "tail." This is an extremely common idiom in F#, particularly because of its thread-safe nature (because all state is held on the stack, rather than in a temporary variable; see Chapter 14 for more on F# and immutable state).

Note, however, that other forms of matching against the list are possible; for example the following is a more verbose way of extracting the first few values of a list:

let message = match numbers with
                | [] -> "List is empty!"
                | [one] ->
                    "List has one item: " + one.ToString()
                | [one; two] ->
                    "List has two items: " +
                    one.ToString() + " " + two.ToString()
                | _ -> "List has more than two items"
System.Console.WriteLine(message)

Again, because the pattern-match happens in a top-down fashion, the wildcard only matches if the list has three or more items in it. In general, however, because this style of list-matching has no convenient way to match against a list of arbitrary length, the preferred manner is to use the head :: tail pattern recursively.

Array Patterns

Just as pattern-matching can match against lists, pattern-matching can also match against arrays:

let numbers = [|1; 2; 3; 4; 5|]
let message = match numbers with
                | [| |] -> "Array is empty!"
                | [| one |] ->
                    "Array has one item: " + one.ToString()
                | [| one; two |] ->
                    "Array has two items: " +
                     one.ToString() + " " + two.ToString()
                | _ -> "Array has more than two items"
System.Console.WriteLine(message)

However, because arrays lack an easy way to "tear off" a piece of the array and recursively process the remainder, as lists do, arrays are rarely used as a source or target of pattern-matching.

Discriminated Union Patterns

Pattern-matching against discriminated unions (described in Chapter 7) is not much different than pattern-matching against other types, with the exception that the compiler sanity-checks to ensure that a match against a discriminated union value only has match clauses that could be remotely possible.

type Color =
    | Black
    | Blue
    | Cyan
    | Gray
    | Green
    | Magenta
    | DarkBlue
    | Red
    | White
    | Yellow

let color = Black
let message = match color with
                | Black -> "Black!"
                | Blue -> "Blue!"
                | _ -> "Something other than black or blue"
System.Console.WriteLine(message)

Discriminated unions are a powerful companion to pattern-matching, just as tuples are.

Record Patterns

Record types, described more in Chapter 7, can also be pattern-matched (and values extracted) in much the same way that tuples are. In fact, since a record type is, in many ways, "just" a tuple, pattern matching with a record type is just an extension of doing it against a tuple type:

type Author =
    | Author of string * string * int

let ted = Author("Ted", "Neward", 38)
match ted with
    | Author(first, last, age) ->
        System.Console.WriteLine("Hello, {0}", first)

Record types are covered in more detail in Chapter 7, and more examples of pattern-matching with records are given there.

PATTERN GUARDS

Despite all the power inherent in the various pattern types, F# developers will still periodically find situations where a pattern-match is clearly the best control construct to use, yet the match criteria just isn't quite a one-to-one match with one of the existing pattern types. For those situations, the F# language provides the capability to put a when clause on the pattern, effectively a Boolean check that must be passed for the clause to be fired:

let p = new Person("Rick", "Minerich", 35)
let message = match (p.FirstName) with
                | _ when p.Age > 30 ->
                    "Never found"
                | "Minerich" when p.FirstName <> "Rick" ->
                    "Also never found"
                | "Minerich" ->
                    "Hiya, Rick!"
                | _ ->
                    "Who are you?"
System.Console.WriteLine("We got {0}", message)

In essence, the when clause gives the F# developer the ability to attach any arbitrary criteria to the pattern-match, above and beyond those criteria defined by the various pattern-match rules already described. This capability can be badly abused, because any number of when clauses can be strung together using the Boolean operators, like so:

let p = new Person("Rick", "Minerich", 35)
let isOldFogey (person : Person) =
    match person with
    | _ when person.Age > 35 ||
        (person.FirstName = "Ted" &&
            person.LastName = "Neward") ||
        (person.FirstName = "Aaron" &&
            person.LastName = "Erickson") ->
        true
    | _ -> false
System.Console.WriteLine("{0} is an old fogey: {1}",
    p, isOldFogey p)

In these cases, it's often better to restructure the code to be more intentional about what the restrictions are, either by breaking the pattern-match into multiple matches, using active patterns, or out into separate functions entirely.

A first refactoring of the preceding code might produce:

let isOldFogey' (person : Person) =
    let isOld (p : Person) =
p.Age > 35
   let isTed (p : Person) =
       p.FirstName = "Ted" && p.LastName = "Neward"
   let isAaron (p : Person) =
       p.FirstName = "Aaron" && p.LastName = "Erickson"
   match p with
       | _ when isOld p || isTed p || isAaron p ->
           true
       | _ ->
          false
System.Console.WriteLine("{0} is an old fogey: {1}",
    p, isOldFogey' p)

But it should be pointed out that it's possible to completely rewrite the match to take advantage of pattern-matching more effectively, and reduce the use of the when to its minimalist case:

let isOldFogey" (p : Person) =
    match p.Age, p.FirstName, p.LastName with
    | _, "Ted", "Neward" -> true
    | _, "Aaron", "Erickson" -> true
    | a, _, _ when a > 35 -> true
    | _ -> false
System.Console.WriteLine("{0} is an old fogey: {1}",
    p, isOldFogey" p)

For the most part, when tempted to use a when clause, the neophyte F# developer should take an extra moment to see if another pattern-matching construct can achieve the same effect.

Note

The use of the apostrophe and double apostrophe, deriving from the "prime" and "double-prime" in mathematics, in the two refactorings above may throw the hardened C# developer off (as it did one reviewer of the book). While legal, the use of the apostrophe may make it hard for F# neophytes to understand the syntax, and it will certainly make it hard for other .NET languages to consume a function or member using it in the name. As a result, F# developers should probably consider restricting its use to locally-scoped members (which won't be seen by external users of the code) and for temporary refactorings.

ACTIVE PATTERNS

If there is any criticism of pattern-matching, it is that the pattern-match is, at least so far, restricted to types understood by the F# compiler — meaning no complex composite types, like business object types, are eligible. This has historically meant that functional languages avoided "abstraction" in the traditional object-oriented sense, in favor of a "lightweight abstraction" by simply bundling data elements together and calling it a data type.

With F#, this limitation more or less goes away with the introduction of active patterns. In essence, active patterns are functions usable from within pattern-match rules, providing a degree of flexibility and extensibility beyond that of pattern-matching described thus far.

Active patterns can be thought of in three general forms: a single-case active pattern, which is used to convert data from one type to another for use in a pattern-match; a partial-case active pattern, which helps to match when data-conversion failures are possible or likely; and a multi-case active pattern, which can take the input data and break it into one of several different groupings.

Single Case

The single-case active pattern is conceptually the easiest to understand: It allows the F# developer to specify a function that is called from inside the pattern-match — rather than before the match — so as to allow for processing or conversion of the data.

For example, when a developer starts to get used to using pattern-matching, it's fairly common to want to use it to handle all sorts of multi-case logic, such as situations where we want to determine if text matches a particular input pattern. It's not too difficult to imagine validation cases in a web application where a developer wants to determine if a text string contains "dangerous" input, such as embedded HTML tags of some form:

let inputData = "This is some <script>alert()</script> data"
let contains (inStr : string) =
    inStr.Contains "<script>"
System.Console.WriteLine("Does the text contain bad data? " +
    (contains inputData).ToString())

Obviously a real implementation would be screening for much more beyond <script> tags, so a refinement is necessary:

let inputData = "This is some <script>alert()</script> data"
let contains (srchStr : string) (inStr : string) =
    inStr.Contains srchStr
System.Console.WriteLine("Does the text contain bad data? " +
    (contains "<script>" inputData).ToString())

But using this requires us to list out all the possible "bad tags" that might appear in this user-input string, which can get tedious:

let inputData = "This is some <script>alert()</script> data"
let contains (srchStr : string) (inStr : string) =
    inStr.Contains srchStr
let goodInput inStr =
    contains "<script>" inStr ||
    contains "<object>" inStr ||
    contains "<embed>" inStr ||
    contains "<applet>" inStr
System.Console.WriteLine("Does the text contain bad data? " +
    (goodInput inputData).ToString())

Frankly, this is precisely the kind of thing that pattern-matching was originally intended to model, to avoid having to write this kind of code. Unfortunately, attempting to use contains() inside the pattern-match causes the pattern-match to match on the results of the contains() call, which isn't quite what's needed here, particularly because we need to call contains() over and over again (once for each potentially "bad" input).

Enter the single-case active pattern. By defining an active pattern — a function defined with "banana clips" around it — the function can be called as part of the pattern-match, as in:

let (|Contains|) srchStr (inStr : string) =
    inStr.Contains srchStr
let isSafeHtml inputData =
    match inputData with
    | Contains "<script>" true -> false
    | Contains "<object>" true -> false
    | Contains "<embed>" true -> false
    | Contains "<applet>" true -> false
    | _ -> true

Viewed this way, it may not seem like the active pattern is saving much time or space, but the logical equivalent, when written out, would be a series of nested "if" or pattern-match statements, testing each possibility one-by-one before finally resolving to some result.

The parameters to the Contains pattern function don't seem to match precisely against the use inside the pattern-match itself. This is because the first parameter (srchStr in the Contains() example) is coming from the match clause (<script>), and the second parameter (inStr) from the value being matched against (inputData). Regardless of the number of parameters to the active recognizer (the term the F# language uses for the Contains() function), the last parameter in the function definition is always the value being matched. The true at the end of the match clause is the actual test — if the Contains() call returns true, it matches against the value in the test (the literal "true"), and thus the match clause succeeds.

Partial Case

The partial-case active pattern is, in many ways, a variant of the single-case active pattern, in that it allows for matches that may or may not always match successfully; in other words, it enables a "partitioning" of the input space into one or more possible variations. To understand what that means, let's return to the previous input-validation scenario. Much of the input validation itself would be made easier if we could incorporate a regular expression into the mix — in other words, if we could "partition out" the input into either something that matched a given regex or not. To do so, we can write a partial-case active pattern — again using the "banana clips" notation — to define it:

let inputData = "This is some <script>alert()</script> data"

let (|Contains|_|) pat inStr =
    let results =
        (System.Text.RegularExpressions.Regex.Matches(inStr, pat))
    if results.Count > 0
       then Some [ for m in results -> m.Value ]
       else None

let matchStuff inData =
    match inData with
    | Contains "http://S+" _ -> "Contains urls"
    | Contains "[^@]@[^.]+.W+" _ -> "Contains emails"
    | Contains "<script>" _ -> "Found <script>"
    | Contains "<object>" _ -> "Found <object>"
    | _ -> "Didn't find what we were looking for"

System.Console.WriteLine(matchStuff inputData)

The defining characteristic of a partial-case pattern match is twofold; first, the "wildcard" character defined as part of the function's name (such as (|Contains|_|) in the preceding code), intended to convey the idea that this is not always necessarily going to yield a successful result; and second, the return of the function being an Option type, with Some returning the results (which we ignore in our example), and None implying that the match didn't work.

Note that we can have numerous partial-case pattern matches; in fact, we can have as many as might be necessary to help complete the match. So, to use a different example (drawing from the Person class defined in Chapter 8), we can write several partial-case pattern match constructs to make it easier to use Reflection to find a particular part of a type:

let AllBindingFlags =
    BindingFlags.NonPublic ||| BindingFlags.Public |||
    BindingFlags.Instance ||| BindingFlags.Static
let (|Field|_|) name (t : System.Type) =
    let fi = t.GetField(name, AllBindingFlags)
    if fi <> null then Some(fi) else None
let (|Method|_|) name (t : System.Type) =
    let fi = t.GetMethod(name, AllBindingFlags)
    if fi <> null then Some(fi) else None
let (|Property|_|) name (t : System.Type) =
    let fi = t.GetProperty(name, AllBindingFlags)
    if fi <> null then Some(fi) else None
let pt = (new Person("", "", 0)).GetType()
let message =
    match pt with
    | Property "FirstName" pi ->
        "Found property " + pi.ToString()
    | Property "LastName" pi ->
        "Found property " + pi.ToString()
    | _ -> "There's other stuff, but who cares?"
System.Console.WriteLine(message)

Note

Note that this example assumes that the System.Reflection namespace has been opened already, as described in Chapter 11.

Contrary to what a Reflection-based example might imply, and what the preceding syntax seems to reinforce, this code won't find all the different properties, fields, and methods on the type passed in; instead, as is always the case with pattern-matching, it finds the first match that succeeds and returns that as the result of the pattern-match expression. This means that the partial-case pattern is good for finding a particular element (such as the FirstName property on any passed-in object) before extracting its value from that object:

let AllBindingFlags =
    BindingFlags.NonPublic ||| BindingFlags.Public |||
    BindingFlags.Instance ||| BindingFlags.Static
let (|Field|_|) name (inst : obj) =
    let fi = inst.GetType().GetField(name, AllBindingFlags)
    if fi <> null
then Some(fi.GetValue(inst))
    else None
let (|Method|_|) name (inst : obj) =
    let fi = inst.GetType().GetMethod(name, AllBindingFlags)
    if fi <> null
    then Some(fi)
    else None
let (|Property|_|) name (inst : obj) =
    let fi = inst.GetType().GetProperty(name, AllBindingFlags)
    if fi <> null
    then Some(fi.GetValue(inst, null))
    else None

let rm = new Person("Rick", "Minerich", 29)
// Does it have a first name? Get the value if it does
let message = match rm with
                | Property "FirstName" value ->
                    "FirstName = " + value.ToString()
                | _ -> "No FirstName to be found"
System.Console.WriteLine(message)

This is sometimes known as duck typing ("If it walks like a duck and talks like a duck, treat it as a duck") in dynamic languages such as Python and Ruby.

This is also where AND patterns, briefly mentioned in the section "AND, OR Patterns" can be useful; if the object in question needs both a FirstName property and a LastName property before it can be considered a good object to work with, then the AND in the pattern-match will require both properties to be there before evaluating the action after the arrow:

let rm = new Person("Rick", "Minerich", 29)
// Does it have a first name AND a last name?
let message = match rm with
                | Property "FirstName" fnval &
                  Property "LastName" lnval ->
                     "Full name = " + fnval.ToString() +
                     " " + lnval.ToString()
                | Property "FirstName" value ->
                    "Name = " + value.ToString()
                | Property "LastName" value ->
                    "Name = " + value.ToString()
                | _ ->
                    "No name to be found"
System.Console.WriteLine(message)

Partial-case active patterns can obviously be used in a variety of other scenarios beyond Reflection; one such area is in processing XML documents, where judicious use of active patterns can act as a structured and type-safe form of XPath query. (More on using XML with F# can be found in Chapter 20.)

Multi-Case

The previous Reflection-oriented example suffers from the fact that the match types a Type and uses that to try and find a match in top-down order, which succeeds the first time and then quits the rest of the match expression. This is fine if we want to test to see if a particular type supports a particular operation, but if the goal is to break down a given type into its constituent parts (such as what we see with tools such as ILDasm or Reflector), then the multi-case active pattern comes into play:

let (|Property|Method|Field|Constructor|) (mi : MemberInfo) =
    if (mi :? FieldInfo) then
        Field(mi.Name, (mi :?> FieldInfo).FieldType)
    elif (mi :? MethodInfo) then
        Method(mi.Name, (mi :?> MethodInfo).ReturnType)
    elif (mi :? PropertyInfo) then
        Property(mi.Name, (mi :?> PropertyInfo).PropertyType)
    elif (mi :? ConstructorInfo) then
        Constructor("", mi.DeclaringType)
    else
        failwith "Unrecognized Reflection type"

Note

Again, this example assumes that System.Reflection has been opened already in the code, as discussed in Chapter 11.

In this case, we use dynamic type tests (see Chapter 9 for details on downcasting) to determine what subtype the MemberInfo instance is and extract values from that subtype for use.

Syntactically, notice that the multi-case active pattern includes all the possible result values within the banana clips, and the lack of the wildcard character implies that when this function is invoked, the results must, somehow, match into one of those values (Property, Method, Field, or Constructor in the previous example). The data that should be returned for use in the pattern-match is gathered into a tuple under the pattern-match name (Property, Method, and so on) and handed back.

When used, the multi-case active pattern (like all pattern-matching constructs) can bind values into a local value binding, which in this case, using the preceding definition, makes it relatively easy to extract the name and type of the reflective element in question:

for p in typeof<Person>.GetMembers(AllBindingFlags) do
    match p with
    | Property(nm, ty) ->
        System.Console.WriteLine(
            "Found prop {1} {0}", nm, ty)
    | Field(nm, ty) ->
        System.Console.WriteLine(
            "Found fld {1} {0}", nm, ty)
    | Method(nm, rt) ->
        System.Console.WriteLine(
            "Found mth {1} {0}(...)", nm, rt)
    | Constructor(_, _) ->
        System.Console.WriteLine("Found ctor")

Having done that, however, something jarring appears: Under the current definition of the multi-case pattern match, we lose the parameters from the method, and the constructor doesn't actually have a name or a return type, per se. This is because as written, the multi-case pattern match assumes that in each case, we want to return a string and a System.Type tuple.

Fortunately, the multi-case active pattern doesn't require that each possibility return the same type; it returns a Choice<> type, essentially creating a large tagged union type out of each of the potential types returned by the individual matches. So we can have the Constructor tuple return just the parameters to the constructor, and the Method tuple return the name, the return type, and the parameters to the method:

let (|Property|Method|Field|Constructor|) (mi : MemberInfo) =
    if (mi :? FieldInfo) then
        Field(mi.Name, (mi :?> FieldInfo).FieldType)
    elif (mi :? MethodInfo) then
        let mthi = (mi :?> MethodInfo)
        Method(mi.Name, mthi.ReturnType, mthi.GetParameters())
    elif (mi :? PropertyInfo) then
        let pi = (mi :?> PropertyInfo)
        Property(pi.Name, pi.PropertyType)
    elif (mi :? ConstructorInfo) then
        let ci = (mi :?> ConstructorInfo)
        Constructor(ci.GetParameters())
    else
        failwith "Unrecognized Reflection type"

This now gives us better data-extraction capabilities when displaying the individual parts of the type:

let pt = (new Person("", "", 0)).GetType()
let AllBindingFlags =
    BindingFlags.NonPublic ||| BindingFlags.Public |||
    BindingFlags.Instance ||| BindingFlags.Static
for p in pt.GetMembers(AllBindingFlags) do
    match p with
    | Property(nm, ty) ->
        System.Console.WriteLine(
            "Found prop {1} {0}", nm, ty)
    | Field(nm, ty) ->
        System.Console.WriteLine(
            "Found fld {1} {0}", nm, ty)
    | Method(nm, rt, parms) ->
         System.Console.WriteLine(
             "Found mth {1} {0}(...)", nm, rt)
    | Constructor(parms) ->
        System.Console.WriteLine("Found ctor")

The huge advantage to this approach, of course, is that as this code seeks to support additional Reflection types (events, delegates, generics, and so on), the active-pattern can simply be extended to add those additional types, and using them simply follows the traditional pattern-match style.

The multi-case active pattern is sometimes called data decomposition because it essentially allows an object (in this case, System.Type instance) to be broken down (decomposed) into its constituent parts and processed.

SUMMARY

Pattern-matching is a fundamental part of the F# language, and of functional languages in general, that it should be considered to be absolutely critical to understanding any code written in F#. It provides well beyond the basic switch/case multi-if capabilities that C# or Visual Basic provide, to give developers the ability to do data decomposition and break complex data structures down into smaller parts for analysis and extraction.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset