Of all the features in F#, pattern matching is the killer app. It’s powerful, accessible, and extensible. Patterns condense a lot of decision-making power into little space without sacrificing readability, as you’ll see in this two-part series.
The ergonomics people at work keep telling us that we need to be more active. Don’t sit at your desk all day, they say. How can we get our work done and be active at the same time? Just tell those ergonauts, "I’m using active patterns!"
The out-of-the-box patterns described in part 1 of this series do some important stuff. Without getting up from their chair, they capture matched values, decompose lists and tuples and discriminated unions, and guard against boolean expressions. But they can’t swim as far as regular expressions, and they can’t catch a pop-fly by a custom type. Custom types need custom patterns to fit them.
Patterns start getting our heart rate up when we start customizing them. With active patterns, we create our own functions to transform, recognize, and categorize custom types. Our match-expressions have Olympic lifting power and the slim figure of a gymnast.
This article builds on part one of this series. If any of the pattern syntax is unfamiliar to you, look there or in the F# language reference. The code samples included here were developed in the interactive console at tryfsharp.org; this is a great tool for following along and experimenting in the language.
To Make a Pattern
Active patterns are all functions, but not all functions are active patterns. The function name begins with a capital letter. A special syntax tells the compiler that the symbol should be available inside a pattern.
let (|IsATron|) (s : string) = s.EndsWith("TRON")
The “(|” and “|)” surrounding the function name are called banana clips. The function accepts a parameter; its type is that of a test-expression. The function doesn’t do the match itself, but rather converts the test-expression into something else, which is then matched against.
let response = match "JessiTRON" with | IsATron true -> "Yeah! TRON!" | x -> "You should change your name to " + x + "iTRON"
In the first pattern in this match expression, IsATron is applied to the test-expression “JessiTRON”. The output of IsATron is then compared against the next token (true). It matches, so the match expression returns “Yeah! TRON!” When the test-expression is an ordinary name like “Ted” or “Pochatkin”, IsATron will return false, which does not match true. The second pattern matches anything, so the ordinary person will be informed that he should change his name.
The active recognizer IsATron is a single-case, complete active pattern. Give it a string and it returns a boolean every time. If a boolean isn’t exciting enough for you, then just wait; later in this article we will study partial and multi-case active recognizers, which are not so predictable.
Transformation with Active Patterns
Because active patterns convert data from one type into another, they’re great for data transformation and validation. What goes in? What comes out? You decide.
Let’s take another example using the taxation theme we established in Part 1 and suppose we have a text file of state tax information where each line looks something like this:
let line = "MO 8.00% 10.00%"
The objective is to generate from each line an instance of StateTax.
type StateTax (s:string, i:float, p:float)= member this.Abbr = s member this.IncomeTaxRate = i member this.PropertyTaxRate = p
We can write a custom function to parse the file then call it within a pattern. The function accepts a string (one line of the file) as input, and returns a three-part tuple containing the interesting values in a format we like.
let (|ParseLine|) (s : string) = let parts = s.Split([|' ';'%'|]) let incomeTax = float parts. / 100.0 let propertyTax = float parts. / 100.0 (parts.,incomeTax, propertyTax)
Using this active recognizer, we can do parsing, transformation, and validation in one short match expression.
let output = match line with | ParseLine (s, i, t) when s = "MO" || s = "TN" -> Some (StateTax(s, i, t)) | wth -> printfn "Unsupported: %s" wth; None
Let’s examine that first match clause in some detail. This is what it does:
- Passes the test-expression (the value in “line”) to ParseLine.
- Takes the output, which is a tuple of type string * float * float, and matches it against the pattern “(s,i,t)”
- Binds the three parts of the tuple into the three variables s, i, and t
- Checks the guard clause, which validates the state abbreviation. It passes, because s is “MO”.
- Evaluates the result expression.
- The result expression returns a new Option containing the populated StateTax.
If our input file contains a line with tax information for Virginia, the match-expression will return the empty Option None.
Thanks to the active recognizer, the test-expression has been transformed into something we can more easily pattern-match against. The active pattern decomposed the input into its relevant parts, leaving the match-expression terse and easy-to-read.
Recognition with Active Patterns
What if the input to the match expression doesn’t always look the same? Perhaps some rows have a different format. The complete active pattern in the last example doesn’t handle that. A partial active pattern has this power: it can decline to match the input.
let (|ParseLine|_|) (s : string) = let parts = s.Split([|' ';'%'|]) if (parts.Length < 4) then None else try let incomeTax = float parts. / 100.0 let propertyTax = float parts. / 100.0 Some (parts.,incomeTax, propertyTax) with | :? System.FormatException -> None
The partial active pattern returns an option type. If this function doesn’t want to match the input, it returns None. If the input fits, it returns Some string*float*float. When None is returned, the match fails, and F# moves on to the next pattern to look for a match.
The partial active pattern is identified by the |_| after its name inside the banana clips. The active recognizer name now means “ParseLine, or not.” N.B. the exception handling here is a variety of pattern matching, but that dialect is not covered in this article.
To use the new partial active pattern, let’s define a matching function. Then we can iterate over a list of lines.
let parse = function | ParseLine (s, i, t) when s = "MO" || s = "TN" -> Some (StateTax(s, i, t)) | ParseLine (s, i, t) -> printfn "Unsupported state: %s" s; None | wth -> printfn "Unsupported format: %s" wth; None
The match expression can distinguish between irrelevant lines that parse and lines that don’t parse at all. We can use this to parse multiple lines.
let lines = ["MO 8.00% 10.00%" "UT 9.00% 5.00%" "Thank you for your interest"] let output = List.map parse lines
Unsupported state: UT Unsupported format: Thank you for your interest
and populates output with a list of StateTax option types. The lines are mapped using our “parse” function. Filtering out the empty option types is a quick exercise in list manipulation, which is very important in F# but not relevant to this article.
Reusable Active Patterns
Complete and partial active recognizers can accept additional arguments. The last argument is always the test-expression. We can generalize our first example to check for any suffix at the end of a string instead of always "TRON". The suffix becomes the first argument to the function.
let (|EndsWith|) suffix (s:string) = s.EndsWith(suffix)
Now we can use the same active recognizer to look for different suffixes.
let response = match "JessiTRON" with | EndsWith "TRON" true -> "Yeah! TRON!" | EndsWith "tron" true -> "A little baby TRON" | x -> "You should change your name to " + x + "iTRON"
This can be a little confusing at first. Look at that first pattern, ’ EndsWith "TRON" true’. Here, EndsWith is a function name and "TRON" is the first argument, but true is not the second argument to the function. Rather, "JessiTRON" is passed as the second argument, and true is compared with the output to test for a match. The last argument to an active recognizer is always the test-expression.
EndsWith is a parameterized active pattern. If we don’t like that boolean in there, we can change it to a partial parameterized active pattern.
let (|EndingWith|_|) suffix (s:string) = if (s.EndsWith(suffix)) then Some s else None let response = match "JessiTRON" with | EndingWith "TRON" _ -> "Yeah! TRON!" | EndingWith "tron" _ -> "A little baby TRON" | x -> "You should change your name to " + x + "iTRON"
In the pattern, we still provide only one argument to EndingWith. This time, we match the output of EndingWith against a string pattern. The wildcard character makes the match work unless EndingWith returns None.
Now that we can define our own patterns, the AND ("&") and OR ("|") pattern conjunctions become more useful. Let’s add one more quick pattern, a StartingWith to correspond to EndingWith:
let (|StartingWith|_|) prefix (s:string) = if (s.StartsWith(prefix)) then Some s else None let response = match "JessiTRON" with | EndingWith "TRON" _ & StartingWith "J" _ -> "the best! J-TRON!" | EndingWith "TRON" _ -> "Yeah! TRON!" | EndingWith "tron" _ | EndingWith "Tron" _ -> "A little baby TRON" | x -> "You should change your name to " + x + "iTRON"
Handy little active patterns like these are quick to write and even quicker to reuse. This is a strength of F# - small pieces of code that combine in flexible ways. It’s like playing with Legos.
Categorization with Active Patterns
The match-expression can accept any kind of data, and it has the ability to distinguish between kinds of data and treat each differently. To see how this works, consider an important categorization tool in F#: the discriminated union. It’s a little bit like an Enum, except that each case has a different combination of data associated with it. For example:
type TaxInfo = | StateTaxInfo of string * float | CountyTaxInfo of string * string * float
In this case, each TaxInfo is either a StateTaxInfo with a string and a float or a CountyTaxInfo with two strings and a float.
Discriminated unions are highly suited to pattern matching, as we can treat each possible type separately. Each type has its own components, and the pattern can put these into variables.
let myTax = CountyTaxInfo("MO","St. Louis County", 0.14) let stateTaxableIncome = 50000.00 let countyTaxableIncome = 25000.00 let result = match myTax with | StateTaxInfo (state, t) -> "Pay " + string (stateTaxableIncome * t) + " to " + state | CountyTaxInfo (state, county, t) -> "Pay " + string (countyTaxableIncome * t) + " to " + county + " in " + state
The result is:
Pay 3500 to St. Louis County in MO
The output can vary according to the input type, with all data conveniently bound.
What if our type is not a discriminated union, but we want that sort of categorization and decomposition? When different circumstances make different bits of data relevant, we teach F# how to treat any type as a discriminated union. This is the power of a multi-case active pattern.
In order to calculate tax in a readable manner, we might divide the states into categories according to their tax policies.
type StateTax (s:string, i:float, p:float)= member this.Abbr = s member this.IncomeTaxRate = i member this.PropertyTaxRate = p let (|IncomeTaxOnly|IncomeAndPropertyTax|PropertyTaxOnly|) (state:StateTax) = match state.IncomeTaxRate, state.PropertyTaxRate with | 0.0,p -> PropertyTaxOnly p | i,0.0 -> IncomeTaxOnly i | i,p -> IncomeAndPropertyTax (i,p)
Here, we have created a multi-case active recognizer that translates a StateTax into one of three different types. Each type has data associated with it, but the data is not the same for every type. The match expression that uses this active recognizer takes advantage of this:
let assessedHomeValue = 200000.00 let income = 80000.00 let estimateTax (state : StateTax)= match state with | PropertyTaxOnly rate -> assessedHomeValue * rate | IncomeTaxOnly rate -> income * rate | IncomeAndPropertyTax (iRate, pRate) -> assessedHomeValue * pRate + (income - assessedHomeValue * pRate) * iRate
Tax is calculated differently based on the type, using only the data relative to that type. We can use this function to find the lowest-tax state to call home. (The List.minBy function ranks list items based on the output of a function that’s applied to each one. It returns the item which yielded the lowest result.)
let states = [StateTax("MO", 0.10, 0.07); StateTax("TN", 0.0, 0.29); StateTax("TX", 0.12, 0.0)] let result = List.minBy estimateTax states printfn "You could live in %s for %f" result.Abbr (estimateTax result)
The resulting output for which is:
You could live in TX for 9600.000000
Multi-case patterns must always be complete: for every input they must return one of their types. (There is a limit of seven possible cases.) Multi-case active recognizers cannot accept additional arguments. In this way they are more rigid than a single-case active recognizer, but they serve a different purpose: to categorize input and extract the relevant data.
Active patterns give us flexibility to transform, extract, and decide. All active patterns have the power to transform the test-expression into another type. Partial active patterns have the option to pass the ball. Single-case active patterns gain reusability through additional arguments. Multi-case patterns get to call the play, choosing which type and which data to return. Put this together, and you have all the power in the world for string parsing or custom-type discernment, while the match expression remains short and readable. Pattern matching in F# is reason enough to start using this language, and using this language can expand your brain in ways you never expected.