Introduction to Functional Programming in F# – Part 7
Introduction
Welcome to the seventh post in this introductory series on functional programming in F#. In this post we will be extending our knowledge of Pattern Matching by looking at how we can write our own matchers with Active Patterns. There are a number of different Active Patterns available but we will be concentrating on Partial & Multi-Case in this post.
Setting Up
All of the code can be written in F# script files (.fsx) and run using F# Interactive.
Partial Active Patterns
We are going to start with an old Interview favourite - FizzBuzz. Let's have a go at a simple solution;
let calculate i = if i % 3 = 0 && i % 5 = 0 then "FizzBuzz" elif i % 3 = 0 then "Fizz" elif i % 5 = 0 then "Buzz" else i |> string [1..15] |> List.map calculate
It works but can we do better with Pattern Matching? How about this?
let calculate i = match (i % 3, i % 5) with | (0, 0) -> "FizzBuzz" | (0, _) -> "Fizz" | (_, 0) -> "Buzz" | _ -> i |> string
or this?
let calculate i = match (i % 3 = 0, i % 5 = 0) with | (true, true) -> "FizzBuzz" | (true, _) -> "Fizz" | (_, true) -> "Buzz" | _ -> i |> string
None of these is very readable. How about if we could do something like this?
let calculate i = match i with | IsDivisibleBy 3 & IsDivisibleBy 5 -> "FizzBuzz" | IsDivisibleBy 3 -> "Fizz" | IsDivisibleBy 5 -> "Buzz" | _ -> i |> string
We can use a Partial Active Pattern to do this:
let (|IsDivisibleBy|_|) divisor n = // int -> int -> unit option if n % divisor = 0 then Some () else None
The (|..|) are colloquially called 'banana clips'. Note the single '&' to apply both parts of the pattern match in the calculate function.
This is Ok but what happens if we need to add 7 -> Bazz into the mix? Look at the code now!
let calculate i = match i with | IsDivisibleBy 3 & IsDivisibleBy 5 & IsDivisibleBy 7 -> "FizzBuzzBazz" | IsDivisibleBy 3 & IsDivisibleBy 5 -> "FizzBuzz" | IsDivisibleBy 3 & IsDivisibleBy 7 -> "FizzBazz" | IsDivisibleBy 5 & IsDivisibleBy 7 -> "BuzzBazz" | IsDivisibleBy 3 -> "Fizz" | IsDivisibleBy 5 -> "Buzz" | IsDivisibleBy 7 -> "Bazz" | _ -> i |> string
Maybe not such a good idea? How confident would you be that you had covered all of the permutations? Maybe a different approach would yield a better result?
let calculate mapping n = let isDivisibleBy (divisor, result) = if n % divisor = 0 then result else "" let handleEmpty input = if input = "" then n |> string else input mapping |> List.map isDivisibleBy |> List.fold (+) "" |> handleEmpty
This is much nicer. The fold function will concatenate all of the strings generated in the map to the original empty string.
To run the function we can use:
[1..15] |> List.map (fun n -> ([(3, "Fizz"); (5, "Buzz")], n) ||> calculate) [1..15] |> List.map (fun n -> ([(3, "Fizz"); (5, "Buzz"); (7, "Bazz")], n) ||> calculate)
Let's have a look at another Interview question - Leap Years. The basic F# function looks like this:
let isLeapYear year = year % 400 = 0 || (year % 4 = 0 && year % 100 <> 0)
But can Partial Active Pattern help make it more readable?
let (|IsDivisibleBy|_|) divisor n = if n % divisor = 0 then Some () else None let (|NotDivisibleBy|_|) divisor n = if n % divisor <> 0 then Some () else None let isLeapYear year = match year with | IsDivisibleBy 400 -> true | IsDivisibleBy 4 & NotDivisibleBy 100 -> true | _ -> false
There's more code but you could easily argue that it is more readable? We could also do a similar thing to the original with helper functions rather than Active Patterns;
let isDivisibleBy divisor year = year % divisor = 0 let notDivisibleBy divisor year = not (year |> isDivisibleBy divisor) let isLeapYear year = year |> isDivisibleBy 400 || (year |> isDivisibleBy 4 && year |> notDivisibleBy 100)
All three approaches are valid and it's down to preference which is best.
One area where Partial Active Patterns are really helpful is in validation and parsing. This is an example of parsing a string to a DateTime:
open System let (|IsValidDate|_|) input = // string -> DateTime option let (success, value) = DateTime.TryParse(input) if success then Some value else None let parse input = match input with | IsValidDate d -> printfn "%A" d | _ -> printfn "%s is not a valid date" input let isDate = parse "2019-12-20" // 2019-12-20 00:00:00 let isNotDate = parse "Hello" // Hello is not a valid date
The only difference here is that we are returning the parsed value from the Partial Active Pattern instead of unit.
Multi-Case Active Patterns
Multi-Case Active Patterns are different from Partial Active Patterns in that they allow more than one choice and return the choice rather than an option type. The maximum number of choices supported is currently seven.
I play (rather unsuccessfully) in an online Football (the sport where participants kick a spherical ball with their feet rather than throw an egg-shaped object with their hands) Score Predictor.
The rules are simple;
300 points for predicting the correct score (2-3 vs 2-3)
100 points for predicting the correct result (2-3 vs 0-2)
15 points per home goal & 20 points per away goal using the lower of the predicted and actual scores
We have some sample predictions, actual scores and points that we can use to validate the code we write;
(0, 0) (0, 0) = 400 // 300 + 100 + 0 * 15 + 0 * 20 (3, 2) (3, 2) = 485 // 300 + 100 = 3 * 15 * 2 * 20 (5, 1) (4, 3) = 180 // 100 + 4 * 15 + 1 * 20 (2, 1) (0, 7) = 20 // 0 * 15 + 1 * 20 (2, 2) (3, 3) = 170 //100 + 2 * 15 + 2 * 20
Firstly we create a simple tuple type to represent a score;
type Score = int * int
To determine if we have a correct score, we need to check that the prediction and the actual score are the same. Since F# uses structural equality rather than reference equality, this is trivial with a partial active pattern;
let (|CorrectScore|_|) (expected:Score, actual:Score) = if expected = actual then Some () else None
We also need to determine what the result of a Score is. This can only one of three choices; draw, home win or away win. We can easily represent this with a multi-case active pattern;
let (|Draw|HomeWin|AwayWin|) (score:Score) = match score with | (h, a) when h = a -> Draw | (h, a) when h > a -> HomeWin | _ -> AwayWin
Note that this active pattern returns one of the pattern choices rather than an option type. We can now create a new partial active pattern for determining if we have predicted the correct result;
let (|CorrectResult|_|) (expected:Score, actual:Score) = match (expected, actual) with | (Draw, Draw) -> Some () | (HomeWin, HomeWin) -> Some () | (AwayWin, AwayWin) -> Some () | _ -> None
Without the multi-case active pattern, we would have to write something like this;
let (|CorrectResult|_|) (expected:Score, actual:Score) = match (expected, actual) with | ((h, a), (h', a')) when h = a && h' = a' -> Some () | ((h, a), (h', a')) when h > a && h' > a' -> Some () | ((h, a), (h', a')) when h < a && h' < a' -> Some () | _ -> None
I prefer the version using the Multi-Case Active Pattern. Now we need to create a function to work out the points for the goals scored;
let goalsScore (expected:Score) (actual:Score) = let (h, a) = expected let (h', a') = actual let home = [ h; h' ] |> List.min let away = [ a; a' ] |> List.min (home * 15) + (away * 20)
We now have all of the parts to create our function to calculate the total points for each game;
let calculatePoints (expected:Score) (actual:Score) = let pointsForCorrectScore = match (expected, actual) with | CorrectScore -> 300 | _ -> 0 let pointsForCorrectResult = match (expected, actual) with | CorrectResult -> 100 | _ -> 0 let pointsForGoals = goalsScore expected actual pointsForCorrectScore + pointsForCorrectResult + pointsForGoals
There is a bit of duplication/similarity that we will resolve later but firstly we should use our test data to validate our code;
let assertnoScoreDrawCorrect = calculatePoints (0, 0) (0, 0) = 400 let assertHomeWinExactMatch = calculatePoints (3, 2) (3, 2) = 485 let assertHomeWin = calculatePoints (5, 1) (4, 3) = 180 let assertIncorrect = calculatePoints (2, 1) (0, 7) = 20 let assertDraw = calculatePoints (2, 2) (3, 3) = 170
We can simplify our goalsScore fumction using some built in functionality of a tuple - fst and snd;
let goalsScore (expected:Score) (actual:Score) = let home = [ fst expected; fst actual ] |> List.min let away = [ snd expected; snd actual ] |> List.min (home * 15) + (away * 20)
We can also simplify the calculatePoints functions by combining the pattern matching for CorrectScore and CorrectResult into a new function;
let resultScore (expected:Score) (actual:Score) = match (expected, actual) with | CorrectScore -> 400 | CorrectResult -> 100 | _ -> 0
Note that we had to return 400 from CorrectScore in this function as we are no longer able to add the CorrectResult points later. This allows us to simplify the calculatePoints function;
let calculatePoints (expected:Score) (actual:Score) = let pointsForResult = resultScore expected actual let pointsForGoals = goalsScore expected actual pointsForResult + pointsForGoals
We can use a Higher Order function to remove the duplication;
let calculatePoints (expected:Score) (actual:Score) = [ resultScore; goalsScore ] |> List.sumBy (fun f -> f expected actual)
List.sumBy is equivalent to List.map followed by List.sum.
There are other types of Active Pattern that we haven't met in this post; I recommend that you investigate the F# Documentation to make yourself familiar with them.
Conclusion
Active Patterns are very useful (and powerful) features but they are not always the best approach.
In the next post we will be taking some of the features we have met in this post to add validation to the code we used in Post 6.
If you have any comments on this series of posts or suggestions for new ones, send me a tweet (@ijrussell) and let me know.