Partial Active Patterns

There are four kinds of active patterns. We’ve already looked at the Single and Multicase Active Patterns. We’ll now look at Partial Active Pattern.

Partial Active Pattern

A partial active pattern is used when the match expression needs to be transformed in different ways for the individual patterns. Partial active patterns are mutually exclusive in that they don’t necessarily have to relate to the other patterns.

We define the partial active pattern using the banana clips, a label, and an underscore. We identify a successful match by returning Some value and a non match with None.

let (|StartsWithCapital|_|) (v : string) = 
    if v.[0] |> System.Char.IsUpper then Some v
    else None

We can use the active pattern in our match expressions for readability.

let processFile (filePath : string) =
    filePath |> System.IO.File.ReadAllLines
      |> Seq.filter (function StartsWithCapital _ -> true
                            | _                   -> false)

Our processFile function takes in a file, reads all the lines, and then gives us a sequence of lines that begin with a capital letter. There is an issue though. We are using an indexer (v.[0]) on the string in our active pattern which will fail if the value is null or empty. Let’s add another partial active pattern.

let (|NullOrEmptyLine|_|) (v : string) = 
    if System.String.IsNullOrEmpty v then Some ()
    else None

The filter of the processFile will now look like:

Seq.filter (function NullOrEmptyLine     -> false
                   | StartsWithCapital _ -> true
                   | _                   -> false)

The order in which we place the pattern expressions matters. We have to do the EmptyLine pattern match before the StartsWithCapital or we will end up with the same error as before.

Putting it to use

We’ve received some specifications for interfacing with an old system. The system streams a sentence per line. A sentence is identified by the starting capital letter of the sentence. There are some issues with the system. Every once in a while the system will send a sentence in reverse or it will send garbage data (no starting or ending capital letter). Some lines may be null and they should be converted to newlines. Format the incoming data where all sentences are not reversed and include the empty lines.

Based on the specs above we have four cases:

  • Normal sentence
  • Reversed sentence
  • Empty line
  • Garbage data

We seem to already have 2 partial patterns for this exercise which means we only need one more to fulfill our requirements. The reversed sentence.

let (|EndsWithCapital|_|) (v : string) =
    if v.[v.Length - 1] |> System.Char.IsUpper then Some v
    else None

With the final partial active pattern in place we can setup our pattern match to fix the streamed data.

let reverseString : string -> string = 
    Seq.rev >> Seq.toArray >> (fun s' -> new string(s'))

let mungeLine = function
    | NullOrEmptyLine     -> Some "\n"
    | StartsWithCapital s -> Some s
    | EndsWithCapital s   -> s |> reverseString |> Some
    | _                   -> None

Taking my word that the funny syntax in the reverseString function actually reverses a string, we can see the mungeLine function uses our partial active patterns for validating the data. Partial active patterns have made out match expressive and readable to non coders.

Summary

Partial active patterns give us a means to do pattern matching when it may seem like the patterns have nothing in common. Partial active patterns allow us to transform the value we are matching before evaluating the pattern. We have not added partial active patterns to our collection of tools to create readable and expressive code.