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.

Part 6 Table of Contents Part 8

Zurück
Zurück

Why Was Our Project Successful: Coincidence or Blueprint?

Weiter
Weiter

Introduction to Functional Programming in F# – Part 6