RSS Feed
Download our iPhone app
Browse DevX
Sign up for e-mail newsletters from DevX


Parsing with Active Patterns in F# : Page 2

Discover how to use F#'s active patterns to build prototype parsers that you can test interactively.


Single and Multi-Case Active Patterns

Active patterns let you view arbitrary values through a different lens, much the same way as you gave a different underlying representation to expressions in the two examples above. The key difference from ordinary pattern matching is that active patterns can be used on any value, not just discriminated unions. As you'll see shortly, this makes them a handy and versatile tool in your F# programs.

Consider the following simple active pattern:

open System
let (|Empty|Null|String|) (s: string) =
    if s = null then
    elif s.Trim() |> String.IsNullOrEmpty then
        String (s.Trim())

First, note that active patterns are defined by surrounding each pattern case with pipe (|) characters. Because these are special characters, you always end up enclosing the entire pattern recognizer in parentheses (hence the so called "banana clips").

What the preceding code says is: Take a string and decompose it into one of the three shapes depending on whether it is null, empty, or anything else. Using active patterns, instead of writing checks as you would normally do, now you can pattern match on strings—effectively hiding the details of the underlying string representation and creating a nice model around string values:

let TestString s =
    match s with
    | Null     -> printf "String is null\n"
    | Empty    -> printf "String is empty\n"
    | String _ -> printf "String is non-empty\n"
let _ =
    null  |> TestString
    ""    |> TestString
    "foo" |> TestString

The output is:

String is null
String is empty
String is non-empty

Matching with active patterns works by calling the active pattern recognizer (in the above case (|Empty|Null|String|)—a normal function with a special annotation so it can be used inside match constructs and bind values), which in turn returns one of three shapes it defines. Note that these shapes may also carry values (as in the String case above); if they do you can bind those in the match case.

Besides partitioning values into a closed set of shapes (or put differently: erecting a conceptual model on those values), you can also use active patterns to convert values simply by having a single match case in the active pattern as in the following example:

let (|NiceString|) (s: string) =
    if s = null then
let PrintString s =
    let (NiceString ns) = s
    s |> printf "Nice string=[%s]\n"

Here you used a simple let statement to match the argument and to bind the "nice" string representation of it to ns and printed that value (remember that a let binding also performs pattern matching).

Partial Active Patterns

The active patterns you have seen so far in this article were complete—in other words they represented or viewed the value that was matched using a closed set of shapes. These have the added benefit that the compiler is able to find incomplete matches or match cases that are never reached, just as in ordinary pattern matching. However, you often need a way to define pattern recognizers that may not conclusively commit to any shape. Consider the following example:

let (|PrimeEnding99|_|) (num: int) =
    { 2 .. num |> float |> Math.Sqrt |> int }
    |> Seq.tryFind (fun i -> num % i = 0)
    |> function
        | None ->
            if (num + 1) % 100 = 0 then
                num |> Some
        | _ ->

Here the trailing |_| in the definition indicates that you are defining a partial active pattern. These work slightly differently from complete active patterns, because they always return a value that indicates whether a match occurred. In this example, the (|PrimeEnding99|_|) pattern recognizer takes an integer, and the match succeeds only when the integer is a prime number that ends with 99. In every other case, no successful match is made.

Naturally, when you match with partial active patterns you need to include a catch-all case in order to avoid a compiler warning:

let _ =
    match 199 with
    | PrimeEnding99 _ ->
        printf "199 is a prime ending with 99\n"
    | _ ->
        printf "199 is not a prime\n"

Another useful bit of knowledge about active patterns is that they can be parameterized. This is particularly useful in situations where you need to control how matching should take place, or when the matching process needs external information to proceed. You will see an example of this in the next section.

Close Icon
Thanks for your registration, follow us on our social networks to keep up-to-date