© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
K. EasonStylish F# 6https://doi.org/10.1007/978-1-4842-7205-3_6

6. Pattern Matching

Kit Eason1  
(1)
Farnham, Surrey, UK
 

We may say most aptly, that the Analytical Engine weaves algebraical patterns just as the Jacquard-loom weaves flowers and leaves.

—Ada Lovelace, Computer Pioneer

Weaving Software with Patterns

I have many “favorite” F# features, but my favorite favorite is pattern matching! Perhaps this is because it’s the feature that takes us furthest away from Object-Oriented coding, letting us truly differentiate from legacy coding patterns. Another nice aspect is how unexpectedly pervasive it can be in well-factored code bases. Prepare to be surprised at the places where you can use pattern matching to simplify and beautify your code. But also be prepared to exercise some restraint in using your newfound superpower. Pattern matching can be overdone.

Because pattern matching tends to be completely new, conceptually, to many developers, I’m going to be more gradual in my explanations than I have been in other chapters of this intermediate book. I’ll start with the very basics.

Pattern Matching Basics

At its simplest, pattern matching is analogous to the switch or case constructs found in many languages. For example, Listings 6-1 and 6-2 show how we’d implement simple switching of control using C# and F#.
int caseSwitch = 1;
switch (caseSwitch)
{
    case 1:
        Console.WriteLine("Case 1");
        break;
    case 2:
        Console.WriteLine("Case 2");
        break;
    default:
        Console.WriteLine("Default case");
        break;
}
Listing 6-1

Case switching in C#

let caseSwitch = 2
match caseSwitch with
| 1 -> printfn "Case 1"
| 2 -> printfn "Case 2"
| _ -> printfn "Default case"
Listing 6-2

Case switching in F#

This is explicit pattern matching – that is, we are using the match keyword – and it’s super clear what is going on.

Using pattern matching to match on integer literals like this is a bit like using an expensive torque wrench as a hammer, but even here there are some surprising goodies to be had. Try commenting out the bottom line in Listing 6-2. You’ll get a compiler warning saying “Incomplete pattern matches on this expression. For example, the value '0' may indicate a case not covered by the pattern(s).” The F# compiler checks, at the type level, whether a value that isn’t covered by the listed cases could conceivably be passed in. And just in case you don’t believe it, it gives you an example! I can’t tell you how many times that feature alone has saved my bacon. Incidentally, it’s a good habit to have no warnings in your F# code or even to turn on the “warnings as errors” setting in your development/build environment. F# warnings are almost always pointing you to a genuine weakness in your code, and “Incomplete pattern matches” warnings are the best example of this.

If you want multiple conditions to invoke the same body of code, use several | case constructs and follow the last of them with the -> arrow and then the code to be executed (Listing 6-3).
let caseSwitch = 3
// "Maybe 3, maybe 4"
match caseSwitch with
| 1 -> printfn "Case 1"
| 2 -> printfn "Case 2"
| 3
| 4 -> printfn "Maybe 3, maybe 4"
| _ -> printfn "Default case"
Listing 6-3

Handling multiple match cases

I love how layout and syntax work together here so that you can run your eye down the code and spot anomalies and special cases at a glance. The code is almost a diagram of what you want to happen.

Now let’s start treating the torque wrench with a bit of respect. What else can it do? Well it can recover the value that actually matched at runtime, if you follow the case or cases with as x (Listing 6-4).
let caseSwitch = 3
// "Maybe 3, maybe 4. But actually 3."
match caseSwitch with
| 1 ->
    printfn "Case 1"
| 2 ->
    printfn "Case 2"
| 3 | 4 as x ->
    printfn "Maybe 3, maybe 4. But actually %i." x
| _ ->
    printfn "Default case"
Listing 6-4

Recovering a matched value

Using the as x construct means that an identifier of the appropriate type, called x (or whatever you want to label it), is bound with the value that matched. The scope of this identifier is limited to the code that’s executed as a result of the match. In the code of other cases, and outside the match expression, it has no meaning.1

I like to think of a match expression as a kind of time travel, allowing you to go back and get the value that must have been assigned for this case to have matched.

When Guards

If you want a bit more branching, using the value recovered in a match case, you can use a when guard. A when guard is a bit like an if expression, and it uses the recovered value for some comparison. Only if the comparison returns true is the following code executed (Listing 6-5).
let caseSwitch = 11
// "Less than a dozen"
match caseSwitch with
| 1 ->
    printfn "One"
| 2 ->
    printfn "A couple"
| x when x < 12 ->
    printfn "Less than a dozen"
| x when x = 12 ->
    printfn "A dozen"
| _ ->
    printfn "More than a dozen"
Listing 6-5

Matching with a when guard

Pattern Matching on Arrays and Lists

What if the value being matched is a bit more structured – say, an array? We can pattern match on arrays and pick out cases having specific element counts (Listing 6-6).
let arr0 = [||]
let arr1 = [|"One fish"|]
let arr2 = [|"One fish"; "Two fish"|]
let arr3 = [|"One fish"; "Two fish"; "Red fish"|]
let arr4 = [|"One fish"; "Two fish"; "Red fish"; "Blue fish"|]
module Pond =
    let describe (a : string[]) =
        match a with
        | [||] ->
            "An empty pond"
        | [| fish |] ->
            sprintf "A pond containing one fish: %s" fish
        | [| f1; f2 |] ->
            sprintf "A pond containing two fish: %s and %s" f1 f2
        | _ ->
            "Too many fish to list!"
// An empty pond
// A pond containing one fish: One fish
// A pond containing two fish: One fish and Two fish
// Too many fish to list!
// Too many fish to list!
[| arr0; arr1; arr2; arr3; arr4 |]
|> Array.map Pond.describe
Listing 6-6

Pattern matching on arrays

This process of recovering the constituents of a structured type is often called decomposition .

Array decomposition is a little limited, as you have to specify either arrays of specific sizes (including size zero) or a catch-all case using an underscore. List decomposition is a bit more powerful, taking advantage of the linked structure of a list (Listing 6-7).
let list0 = []
let list1 = ["One fish"]
let list2 = ["One fish"; "Two fish"]
let list3 = ["One fish"; "Two fish"; "Red fish"]
let list4 = ["One fish"; "Two fish"; "Red fish"; "Blue fish"]
module Pond =
    let describe (a : List<string>) =
        match a with
        | [] ->
            "An empty pond"
        | [ fish ] ->
            sprintf "A pond containing one fish only: %s" fish
        | head::tail ->
            sprintf "A pond containing one fish: %s (and %i more fish)"
                head (tail |> List.length)
// A pond containing one fish only: One fish
// A pond containing one fish: One fish (and 1 more fish)
// A pond containing one fish: One fish (and 2 more fish)
// A pond containing one fish: One fish (and 3 more fish)
[| list1; list2; list3; list4 |]
|> Array.map Pond.describe
Listing 6-7

Pattern matching on lists

Here, the first two cases are pretty much as Listing 6-6, except we use list brackets [] instead of “array clamps” [||]. The next case uses a cons operator ::. When constructing a list, you can use the cons operator to join a single element onto the beginning of a list (e.g., "One fish" :: [ "Two fish"; "Red fish" ]). But here we are using it in the opposite direction – to recover the first element and all subsequent elements (if any) from an existing list. (You can see now why I referred to pattern matching as a form of time travel: the :: operator works both forwards in time to compose and backwards in time to decompose. You can also think of this as the :: operator being “inverted.”)

In Listing 6-7, I’ve used the identifiers head and tail in the cons case, that is, head::tail. I normally use the names head and tail in cons matching, regardless of the business meanings of the particular values in question. (The alternative in this case might have been something like firstFish::otherFishes.) This is one of those conventions, like using acc and elem in fold functions, which helps your mind recognize common idioms with as little cognitive overhead as possible and saves you from some unnecessary decision-making.

You might want to experiment a bit to prove what I said about the possibility of the tail containing zero elements. Comment out the | [ fish ] -> case from Listing 6-7 and its following printf line. What do you expect to happen when you send list1 into the match expression? Were you right?

Finally, you might have noticed that although the individual “ponds” in Listing 6-7 are of type List<string>, the demonstration in the last two lines makes an array of those lists and uses Array.map to process them. I could just as well have used a list of lists, but it’s also absolutely fine to mix collections as I have done here.

Pattern Matching on Tuples

We’ve got a bit ahead of ourselves and missed out one of the most pervasive forms of pattern matching – so pervasive it’s not that obvious that it is pattern matching at all. Consider a function that returns a tuple. You can call that function and decompose the tuple result straight into separate values like this (Listing 6-8).
let extremes (s : seq<_>) =
    s |> Seq.min,
    s |> Seq.max
// lowest : int = -1
// highest : int = 9
let lowest, highest =
    [1; 2; 9; 3; -1] |> extremes
// -1, 9
lowest, highest
Listing 6-8

Pattern matching on tuples in a let binding

You can also explicitly pattern match on tuples using the match keyword. For example, the Int32.TryParse function returns a tuple consisting of a Boolean flag to say whether the parsing succeeded and the parsed integer value. (The compiler cleverly translates into a tuple result from the “real” signature of TryParse, in which the value is placed in a by-reference parameter.) Thus, you can pattern match to recover the value and place it into an option type, which makes it more usable from the rest of your F# code (Listing 6-9).
open System
let tryParseInt (s : string) =
    match Int32.TryParse(s) with
    | true, i -> Some i
    | false, _ -> None
// Some 30
"30" |> tryParseInt
// None
"3X" |> tryParseInt
Listing 6-9

Pattern matching on tuples using match

Pattern Matching on Records

You can also use pattern matching to decompose record types. This is sometimes useful when you want to pluck one or two values out of the record and ignore the rest.
type Track = { Title : string; Artist : string }
let songs =
    [ { Title = "Summertime"
        Artist = "Ray Barretto" }
      { Title = "La clave, maraca y guiro"
        Artist = "Chico Alvarez" }
      { Title = "Summertime"
        Artist = "DJ Jazzy Jeff & The Fresh Prince" } ]
let distinctTitles =
    songs
    |> Seq.map (fun song ->
        match song with
        | { Title = title } -> title)
    |> Seq.distinct
// seq ["Summertime"; "La clave, maraca y guiro"]
distinctTitles
Listing 6-10

Pattern matching on record types

In Listing 6-10, we pull Title out of the record and ignore Artist. (You aren’t obliged to use Artist = _ to do this; you can just omit the fields you aren’t interested in.). The syntax is a little confusing at first because Title = title looks almost like an assignment but written backward, given that it is title (on the right) that receives the value.

There are more concise ways to achieve what we did in Listing 6-10 (e.g., Seq.map (fun song -> song.Title)) – but it’s worth getting used to record matching in the context of a match expression, as it’ll make things easier to understand when we start to discover record matching in other constructs. In fact, let’s jump ahead a bit and look at one example of pattern matching on records without a match expression.

Say the Track type from Listing 6-10 has a few more fields, and we want to write a function that formats a track name and artist as a menu item. Clearly, that function only cares about two fields from the Track type, but it would be quite nice to be able to throw whole Track instances at the function, without either the caller or the callee having to break out the fields of interest. Listing 6-11 shows how to achieve exactly that.
type TrackDetails = {
    Id : int
    Title : string
    Artist : string
    Length : int }
let songs =
    [ { Id = 1
        Title = "Summertime"
        Artist = "Ray Barretto"
        Length = 99 }
      { Id = 2
        Title = "La clave, maraca y guiro"
        Artist = "Chico Alvarez"
        Length = 99 }
      { Id = 3
        Title = "Summertime"
        Artist = "DJ Jazzy Jeff & The Fresh Prince"
        Length = 99 } ]
// The TrackDetails. prefix is is only needed here to avoid a warning when
// working in Notebooks. (A previous cell defines a record with the same
// field names.)
let formatMenuItem ( { TrackDetails.Title = title; TrackDetails.Artist = artist } ) =
    let shorten (s : string) = s.Substring(0, 10)
    sprintf "%s - %s" (shorten title) (shorten artist)
// Summertime - Ray Barret
// La clave,  - Chico Alva
// Summertime - DJ Jazzy J
songs
|> Seq.map formatMenuItem
|> Seq.iter (printfn "%s")
Listing 6-11

Pattern matching at the function call boundary

The magic happens in the parameter list of formatMenuItem, where we say ( { Title = title; Artist = artist } ). This will cause values called title and artist to be bound with the relevant fields’ values from a Track instance, and they will be available within the function body. Other fields from the record are ignored. See how the Seq.map near the bottom of the listing can send in whole Track instances.

You could argue that this technique offers a whole new paradigm of parameter declaration: an alternative to both the curried style, where you just list the parameters with spaces, meaning the caller can populate as many as it feels like; and the tupled style, where the caller must supply values for all parameters. In this new paradigm, the caller must supply a whole record instance, but the callee only sees some of the values. It can be very useful, but it’s a trick to use sparingly. I’ve come across it in my own code and been confused by it!

Pattern Matching on Discriminated Unions

It’s time for the yin of pattern matching to meet its yang, in the form of Discriminated Unions. A Discriminated Union (DU) is a type that has several labeled cases, each of which may have an associated payload of any type. The payload type associated with each case can be different. Multiple types can be put into the payload of a DU case simply by tupling them together or using another complex type such as a class or record.

You can recover the payload of a DU instance using explicit pattern matching (Listing 6-12). In Listing 6-12, we are modeling readings for two kinds of UK domestic electricity meters. The “Standard” meter is one where your consumption is simply recorded as a single number. The “Economy 7” meter is one where daytime and nighttime consumption is recorded separately and charged at different rates. Clearly, a single “meter read” event will produce one value for standard readings and two (which we absolutely must not mix up) for Economy 7. Given these rules, the code in Listing 6-12 should be fairly self-explanatory. The function MeterReading.format takes a MeterReading instance of either type and formats it appropriately for printing on a bill or web page, using pattern matching to recover the reading(s).
type MeterReading =
    | Standard of int
    | Economy7 of Day:int * Night:int
module MeterReading =
    let format(reading : MeterReading) =
        match reading with
        | Standard reading ->
            sprintf "Your reading: %07i" reading
        | Economy7(Day=day; Night=night) ->
            sprintf "Your readings: Day: %07i Night: %07i" day night
let reading1 = Standard 12982
let reading2 = Economy7(Day=3432, Night=98218)
// "Your reading: 0012982", "Your readings: Day: 0003432 Night: 0098218"
reading1 |> MeterReading.format, reading2 |> MeterReading.format
Listing 6-12

Pattern matching on a DU

If you are at an intermediate level in F#, DUs and pattern matching will be pretty familiar to you. But let me point out some language features in Listing 6-12 that are little used in F# code bases generally and which I think should be used more. First, I’ve assigned labels to each of the readings in the Economy7 case, that is, Economy7 of Day:int * Night:int rather than Economy7 of int*int. Second, I’ve used those labels when instantiating Economy 7 readings, that is, Economy7(Day=3432, Night=98218) rather than Economy7(3432, 98218). (F# doesn’t force you to do this, even if you’ve given labels to the tuple elements in the case declaration.) Finally, when decomposing out the day and night values in the pattern match, I’ve again used the labels, that is, | Economy7(Day=day; Night=night) rather than | Economy7(day, night). There’s an oddity in the decomposition part: note how the decomposition syntax has a semicolon, while when you construct the instance, you used a comma (Table 6-1).
Table 6-1

DU Labeled Payload Elements Construction and Decomposition Syntax

Action

Syntax

Construction

Economy7(Day=3432, Night=98218)

Decomposition

Economy7(Day=day; Night=night)

I suspect there is a reason for this: here, the decomposition isn’t quite the “opposite” of the composition because in the decomposition, you can legitimately omit some of the items from the payload. For example, if you just wanted to pull out the day reading, you could use the match case | Economy7(Day=day) -> ....

Anyway, if you choose not to label the items in your payload, Listing 6-13 shows the same functionality as Listing 6-12, but without the labels.
type MeterReading =
    | Standard of int
    | Economy7 of int * int
module MeterReading =
    let format(reading : MeterReading) =
        match reading with
        | Standard reading ->
            sprintf "Your reading: %07i" reading
        | Economy7(day, night) ->
            sprintf "Your readings: Day: %07i Night: %07i" day night
let reading1 = Standard 12982
let reading2 = Economy7(3432, 98218)
// "Your reading: 0012982", "Your readings: Day: 0003432 Night: 0098218"
reading1 |> MeterReading.format, reading2 |> MeterReading.format
Listing 6-13

DUs and pattern matching without payload labels

You will see code like Listing 6-13 much more often, but I prefer the style of Listing 6-12 if there is any possibility of confusion between the elements of a DU payload, or if the nature of the payload isn’t immediately obvious from context. Remember: motivational transparency !

Another alternative to labeling the payload elements is to have the payload as a whole be a type with some structure, for example, a record type. Thus, the field labels or member names make the code self-documenting, taking the place of the payload element labels. Using a “proper” type is obviously a less minimalist approach than simply having labels in the DU payload (and generally I like minimalism), but obviously, it has benefits if you have broader uses for the type anyway.

Pattern Matching on DUs in Function Parameters

If you think back to the section “Pattern Matching on Records,” you might remember that we said you can pattern match in the declaration of a function, thus:
let formatMenuItem ( { Title = title; Artist = artist } ) = ...
In this way, you can recover items from the incoming type and use their values within the function body. It might occur to you that the same should be possible for Discriminated Unions. And yes, you can – with certain important restrictions. Imagine you are trying to implement complex numbers. For this example, all you need to know about complex numbers is that each one has two components, the real and imaginary parts, and that to add two complex numbers, you add each one’s real parts and each one’s imaginary parts and make a new complex number using the two results. (Incidentally, there is no need, in reality, to implement complex numbers, as they are already right there in System.Numerics. Nonetheless, they do make a useful example.) Listing 6-14 shows how you could model complex numbers using a single-case DU.
type Complex =
    | Complex of Real:float * Imaginary:float
module Complex =
    let add (Complex(Real=r1;Imaginary=i1)) (Complex(Real=r2;Imaginary=i2)) =
        Complex(Real=(r1+r2), Imaginary=(i1+i2))
let c1 = Complex(Real = 0.2, Imaginary = 3.4)
let c2 = Complex(Real = 2.2, Imaginary = 9.8)
// Complex(Real=2.4, Imaginary=13.2)
let c3 = Complex.add c1 c2
c3
Listing 6-14

Implementing complex numbers using a single-case DU

Note how, as in Listing 6-12, I’ve opted to label the two components of the payload tuple, as it is rather critical we don’t mix up the real and imaginary components! The key new concept here is the add function , where I’ve done pattern matching in the parameter declaration to pull out the actual values we need for the computation. In the body of the add function, we simply construct a new Complex instance , doing the necessary computation at the same time. Once again, we use the slightly odd semicolon-based syntax at the decomposition stage, even though we compose the instances using commas.

Exactly as with records, this technique can be useful in certain circumstances. But it is a double-edged sword in terms of readability, particularly for nonadvanced maintainers of your code. I would say I’ve regretted using single-case DUs in the manner outlined in Listing 6-14 about as often as I’ve been pleased with the results.

I mentioned “certain important restrictions” when you want to do pattern matching in a function declaration. Apart from the readability risk, the main restriction is that the DU you are using should be a single-case one, or the pattern you use should cover all the possibilities. Consider Listing 6-15, where I have extended the complex number example from Listing 6-14 so that we can have either a “real” number, which is just an everyday floating-point number, or the complex number we described earlier.
type Number =
    | Real of float
    | Complex of Real:float * Imaginary:float
module Number =
    // Warning: Incomplete pattern matches on this expression...
    let add (Complex(Real=r1;Imaginary=i1)) (Complex(Real=r2;Imaginary=i2)) =
            Complex(Real=(r1+r2), Imaginary=(i1+i2))
Listing 6-15

Pattern matching in function declaration on a multicase DU

This immediately causes a compiler warning where the add function is declared because the function only handles one of the two DU cases that could be sent to it. Never ignore this kind of warning: if the DU truly needs to have multiple cases, you will have to refactor any code that uses it to handle all the cases. Failing to do so will undermine the whole edifice of type safety that using F# lets you construct.

You could extend the parameter binding to cover all the cases, as in Listing 6-16.
type Number =
    | Real of float
    | Complex of Real:float * Imaginary:float
module Number =
    // Gets rid of the compiler warning but doesn't make much sense!
    let addReal (Complex(Real=a)|Real(a)) (Complex(Real=b)|Real(b)) =
        Real(a+b)
Listing 6-16

Handling a multicase DU in a function parameter

Leaving aside whether this is a mathematically valid operation, this really isn’t terribly readable, and I struggle to think of a good reason to do it, except perhaps in rather specialized code.

Pattern Matching in Let Bindings

There’s yet another place you can use DU pattern matching: directly in let bindings . If you have a complex number, stored as a single-case DU as in Listing 6-14, you can recover its components directly in a let binding (Listing 6-17).
type Complex =
    | Complex of Real:float * Imaginary:float
let c1 = Complex(Real = 0.2, Imaginary = 3.4)
let (Complex(real, imaginary)) = c1
// 0.2, 3.4
real, imaginary
Listing 6-17

Pattern matching in a let binding

You can also use the component labels if you want to, that is:
        let (Complex(Real=real; Imaginary=imaginary)) = c1

Note that when using labels like this, you must use a semicolon separator rather than a comma, as we saw earlier.

If you want an assign from a multicase DU, you can do so using the | character, providing you bind the same value in all cases and use _ to ignore “leftover” values (Listing 6-18).
type Complex =
    | Real of float
    | Complex of Real:float * Imaginary:float
let c1 = Complex(Real = 0.2, Imaginary = 3.4)
let (Complex(real, _)|Real (real)) = c1
// 0.2
real
Listing 6-18

A let binding from a multicase DU

As I said in the previous section, the times where it is useful and advisable to do this are fairly rare.

Pattern matching in let bindings is a really useful trick once you get used to it. But do bear in mind the readability implications based on the skill level of your collaborators. Don’t do it just to look clever!

Revisiting Single-Case Discriminated Unions

Now that we’ve looked at pattern matching in a number of contexts, let’s revisit the use of Single-Case Discriminated Unions , which we first looked at in connection with railway miles and yards back in Chapter 2. What is the most concise syntax we can think of to validate or clean values on creation of such a DU and to retrieve the wrapped value in a caller-friendly way? This time I’ll use heading as an example. A heading is a direction of travel measured clockwise from North and has values between 0.0° and 359.999…°. If some operation takes the value clockwise past 359.999°…, or anticlockwise past 0°, we need to “wrap around” appropriately. For example, 5° clockwise from 359° is 4°, and 5° anticlockwise from 4° is 359°. Similarly, if something creates a heading using an out-of-range value, say, 361°, we also wrap it around, in this case, to 1°. Listing 6.19 shows one way to achieve all this.2
module Heading =
    [<Struct>]
    type Heading =
        private Heading of double
            member this.Value = this |> fun (Heading h) -> h
    let rec create value =
        if value >= 0.0 then
            value % 360.0 |> Heading
        else
            value + 360.0 |> create
// "Heading: 180.0"
let heading1 = Heading.create 180.0
printfn "Heading: %0.1f" heading1.Value
// "Heading: 90.0"
let heading2 = Heading.create 450.0
printfn "Heading: %0.1f" heading2.Value
// "Heading: 270.0"
let heading3 = Heading.create -450.0
printfn "Heading: %0.1f" heading3.Value
// "Heading: 270.0"
let heading4 = Heading.create -810.0
printfn "Heading: %0.1f" heading4.Value
Listing 6-19

Expressing a heading as a DU

Parts of Listing 6-19 will already be familiar to you: we use a Single-Case DU to carry a payload value, we place the type inside a module named after the DU, and we make its case (also called Heading) private – which makes it impossible to bypass validation by creating an instance directly. For a (potential) performance gain, we mark the DU with the [<Struct>] attribute. Finally, we provide a create function to do instantiation. In Chapter 2, we used a create function to both validate and instantiate, but here we clean and instantiate: values outside the range 0.0…359.99… are automatically wrapped around. Wrapping values above 360.0 is easy we can use the modulus operator %. Wrapping values below 0.0 requires recursively adding 360.0 until the value is brought into range. There are probably more mathematical ways to achieve this, but I quite like the elegance of the recursion. We have to mark the create function as recursive by saying let rec instead of just let.

But the real innovation is the addition of the member called this.Value. From a caller’s point of view, it allows code to retrieve the wrapped value using, for example, heading1.Value. The member pipes this into a lambda that uses pattern matching to get the wrapped value as h, which is then returned to the caller. Providing a Value member is more succinct from the consumer’s point of view than having to pattern match at the point of consumption. Also note that ignoring the [<Struct>] attribute and the create function a DU with a Value member part can even be achieved as a “one liner” (Listing 6-20).
type Heading = Heading of double member this.Value = this |> fun (Heading h) -> h
Listing 6-20

Expressing a heading as a one-line DU

Pattern Matching in Loops and Lambdas

Sometimes, you have a collection of tuples or records that you want to loop over, either explicitly using for-loops or implicitly using higher-order functions such as iter and map. Pattern matching comes in useful here because it lets you seamlessly transition from the collection items to the items to be used in the body of the for-loop or lambda function (Listing 6-21).
let fruits =
    [ "Apples", 3
      "Oranges", 4
      "Bananas", 2 ]
// There are 3 Apples
// There are 4 Oranges
// There are 2 Bananas
for (name, count) in fruits do
    printfn "There are %i %s" count name
// There are 3 Apples
// There are 4 Oranges
// There are 2 Bananas
fruits
|> List.iter (fun (name, count) ->
    printfn "There are %i %s" count name)
Listing 6-21

Pattern matching in loops

In Listing 6-21, we make a list of tuples and then iterate over it in both a for-loop and a higher-order function style. In both cases, a pattern match in the form of (name, count) lets us recover the values from the tuple, for use in the body code.

You can also do this with Record Types, and there’s an exercise showing that at the end of the chapter. And you can do it with Discriminated Unions, though normally only when they are single case.

Purely as a curiosity, Listing 6-22 shows an example of “cheating” by looping with a pattern match over a multicase Discriminated Union. This code actually works (it will just iterate over the cases that are circles) but isn’t great practice unless your aim is to annoy purists. You will get a compiler warning.
type Shape =
    | Circle of Radius:float
    | Square of Length:float
    | Rectangle of Length:float * Height:float
let shapes =
    [ Circle 3.
      Square 4.
      Rectangle(5., 6.)
      Circle 4. ]
// Circle of radius 3.000000
// Circle of radius 4.000000
// Compiler wanning: "Incomplete matches on this expression..."
for (Circle r) in shapes do
    printfn "Circle of radius %f" r
Listing 6-22

Pattern matching in loop over a multicase DU (bad practice!)

Pattern Matching and Enums

If you want a Discriminated Union to be treated more like a C# enum, you must assign each case a distinct value, where the value is one of a small set of simple types such as byte, int32, and char . Listing 6-23 shows how to combine this feature, together with the Sytem.Flags attribute, to make a simplistic model of the Unix-style file permissions structure.
open System
[<Flags>]
type FileMode =
    | None =    0uy
    | Read =    4uy
    | Write =   2uy
    | Execute = 1uy
let canRead (fileMode : FileMode) =
    fileMode.HasFlag FileMode.Read
let modea = FileMode.Read
let modeb = FileMode.Write
let modec = modea ^^^ modeb
// True, False, True
canRead modea, canRead modeb, canRead modec
Listing 6-23

Simple model of Unix-style file permissions

Here, the DU FileMode can take one of four explicit values, each of which is associated with a bit pattern (000, 001, 010, and 100). We can use the HasFlag property (which is added for us because we used the Flags attribute) to check whether an instance has a particular bit set, regardless of the other bits. We can also bitwise-OR two instances together, using the ^^^ operator.

But beware! As soon as you make a DU into an enum , code can assign to it any value that is compatible with the underlying type, including one not supported by any specified case. For example:
        open Microsoft.FSharp.Core.LanguagePrimitives
        let naughtyMode =
            EnumOfValue<byte, FileMode> 255uy
For the same reason, enum pattern matching that doesn’t contain a default case (“_”) will always cause a compiler warning saying “Enums may take values outside known cases. For example, the value ‘enum<FileMode> (3uy) may indicate a case not covered by the pattern(s).” The compiler knows, at the type level, that any value of the underlying type could be sent in, not just one covered by the specified DU cases (Listing 6-24).
open System
[<Flags>]
type FileMode =
    | None =    0uy
    | Read =    4uy
    | Write =   2uy
    | Execute = 1uy
let describeReadability (fileMode : FileMode) =
    let read =
        // Compiler warning: "Enums may take values outside known cases..."
        match fileMode with
        | FileMode.Read -> "can"
        | FileMode.None
        | FileMode.Write
        | FileMode.Execute -> "cannot"
    printfn "You %s read the file"
Listing 6-24

Pattern matching on an enum DU without a default case

Because it makes a hole in type safety, I always avoid using enum DUs except in very specific scenarios, typically those involving language interop.

Active Patterns

Pattern matching and Discriminated Unions are exciting enough, but there’s more! Active Patterns let you exploit the syntactical infrastructure that exists to support pattern matching, by building your own mapping between values and cases. Once again, because this is a somewhat advanced feature, I’m going to explain Active Patterns from the very beginning. Then we can discuss their stylistic implications.

Single-Case Active Patterns

The simplest form of Active Pattern is the Single-Case Active Pattern. You declare it by writing a case name between (| and |) (memorably termed banana clips), followed by a single parameter, and then some code that maps from the parameter value to the case.

For instance, in Listing 6-25, we have an Active Pattern that takes a floating-point value and approximates it to a sensible value for a currency, which for simplicity we are assuming always has two decimal places.
open System
let (|Currency|) (x : float) =
    Math.Round(x, 2)
// true
match 100./3. with
| Currency 33.33 -> true
| _ -> false
Listing 6-25

A Single-Case Active Pattern

With the Currency Active Pattern in place, we can pattern match on some floating-point value that has an arbitrary number of decimal places (such as 33.333333...) and compare it successfully with its approximated value (33.33).

The code is now nicely integrated with the semantics of pattern matching generally, especially when recovering the matched value. Listing 6-26 shows us using Currency in the three contexts we have seen for other pattern matching: match expressions, let bindings, and function parameters.
open System
let (|Currency|) (x : float) =
    Math.Round(x, 2)
// "That didn't match: 33.330000"
// false
match 100./3. with
| Currency 33.34 -> true
| Currency c ->
    printfn "That didn't match: %f" c
    false
// C: 33.330000
let (Currency c) = 1000./30.
printfn "C: %0.4f" c
let add (Currency c1) (Currency c2) =
    c1 + c2
// 66.66
add (100./3.) (1000./30.)
Listing 6-26

Recovering decomposed values with Active Patterns

Multicase Active Patterns

While Single-Case Active Patterns map any value to a single case, Multicase Active Patterns map any value to one of several cases. Let’s say you have a list of wind turbine model names (I got mine from the USGS wind turbine database here: https://eerscmap.usgs.gov/uswtdb/), and you want to divide these into ones made by Mitsubishi, ones made by Samsung, and ones made by some other manufacturer. (Since we are dealing with unconstrained string input data, it’s essential to provide an “Other” case). Listing 6-27 shows how we might do this using a combination of regular expressions and Multicase Active Patterns.
open System.Text.RegularExpressions
let (|Mitsubishi|Samsung|Other|) (s : string) =
    let m = Regex.Match(s, @"([A-Z]{3})(-?)(.*)")
    if m.Success then
        match m.Groups.[1].Value with
        | "MWT" -> Mitsubishi
        | "SWT" -> Samsung
        | _     -> Other
    else
        Other
// From https://eerscmap.usgs.gov/uswtdb/
let turbines = [
    "MWT1000"; "MWT1000A"; "MWT102/2.4"; "MWT57/1.0"
    "SWT1.3_62"; "SWT2.3_101"; "SWT2.3_93"; "SWT-2.3-101"
    "40/500" ]
// MWT1000 is a Mitsubishi turbine
// ...
// SWT1.3_62 is a Samsung turbine
// ...
// 40/500 is an unknown turbine
turbines
|> Seq.iter (fun t ->
    match t with
    | Mitsubishi ->
        printfn "%s is a Mitsubishi turbine" t
    | Samsung ->
        printfn "%s is a Samsung turbine" t
    | Other ->
        printfn "%s is an unknown turbine" t)
Listing 6-27

Categorizing wind turbines using Multicase Active Patterns and Regex

Listing 6-27 exploits the observation that all (and only) Mitsubishi turbines have model names starting with “MWT,” and Samsung ones start with either “SWT” or “SWT-.” We use a regular expression to pull out this prefix and then some string literal pattern matching to map onto one of our cases. It’s important to note that the Active Pattern is defined using a let binding rather than a type declaration, even though the fact that it has a finite domain of cases makes it feel like a type.

Multicase Active Patterns have a serious limitation: the number of cases is capped at seven. Since I’m pretty sure there are more than seven wind turbine manufacturers, Multicase Active Patterns wouldn’t be a great fit when trying to map every case in the real dataset. You’d have to be content with a more fluid data structure.

Partial Active Patterns

Partial Active Patterns divide the world into things that match by some condition and things that don’t. If we just wanted to pick out the Mitsubishi turbines from the previous example, we could change the code to look like Listing 6-28.
open System.Text.RegularExpressions
let (|Mitsubishi|_|) (s : string) =
    let m = Regex.Match(s, @"([A-Z]{3})(-?)(.*)")
    if m.Success then
        match m.Groups.[1].Value with
        | "MWT" -> Some s
        | _     -> None
    else
        None
// From https://eerscmap.usgs.gov/uswtdb/
let turbines = [
    "MWT1000"; "MWT1000A"; "MWT102/2.4"; "MWT57/1.0"
    "SWT1.3_62"; "SWT2.3_101"; "SWT2.3_93"; "SWT-2.3-101"
    "40/500" ]
// MWT1000 is a Mitsubishi turbine
// ...
// SWT1.3_62 is not a Mitsubishi turbine
turbines
|> Seq.iter (fun t ->
    match t with
    | Mitsubishi m ->
        printfn "%s is a Mitsubishi turbine" m
    | _ as s ->
        printfn "%s is not a Mitsubishi turbine" s)
Listing 6-28

Categorizing wind turbines using Partial Active Patterns

Here, we can pattern match on just two cases – Mitsubishi and “not Mitsubishi,” the latter represented by the default match “_”. Notice that in the nonmatching case, although the Active Pattern doesn’t return a value, you can recover the input value using the “as” keyword and a label (here I used “as s”).

Parameterized Active Patterns

You can parameterize Active Patterns, simply by adding extra parameters before the final one. (The last parameter is reserved for the primary input of the Active Pattern.) Say, for example, you had to validate postal codes for various regions. US postal codes (zip codes) consist of five digits, while UK ones have a rather wacky format consisting of letters and numbers (e.g., “RG7 1DP”). Listing 6-29 uses an Active Pattern, parameterized using a regular expression to define a valid format for the region in question.
open System
open System.Text.RegularExpressions
let zipCodes = [ "90210"; "94043"; "10013"; "1OO13" ]
let postCodes = [ "SW1A 1AA"; "GU9 0RA"; "PO8 0AB"; "P 0AB" ]
let regexZip = @"^d{5}$"
// Simplified: the official regex for UK postcodes is much longer!
let regexPostCode = @"^[A-Z](d|[A-Z]){1,3} d[A-Z]{2}$"
let (|PostalCode|) pattern s =
    let m = Regex.Match(s, pattern)
    if m.Success then
        Some s
    else
        None
// ["90210"; "94043"; "10013"]
let validZipCodes =
    zipCodes
    |> List.choose (fun (PostalCode regexZip p) -> p)
// ["SW1A 1AA"; "GU9 0RA"; "PO8 0AB"]
let validPostCodes =
    postCodes
    |> List.choose (fun (PostalCode regexPostCode p) -> p)
validZipCodes, validPostCodes
Listing 6-29

Using parameterized Active Patterns to validate postal codes

In Listing 6-29, I’ve had to simplify the regular expression used for UK postcodes as the real (government endorsed!) one is too long to fit into book-listing form.

One important point to note about Listing 6-29 is that although the Active Pattern we have defined is a “Complete” one (declared using (|PostalCode|) rather than (|PostalCode|_)), it can still return Some or None as values.

Pattern Matching with “&”

Occasionally, it’s useful to be able to “and” together items in a pattern match. Imagine, for example, your company is offering a marketing promotion that is only available to people living in “outer London” (in the United Kingdom), as identified by their postcode. To be eligible, the user needs to provide a valid postcode, and that postcode must begin with one of a defined set of prefixes. Listing 6-30 shows one approach to coding this using Active Patterns.
open System.Text.RegularExpressions
let (|PostCode|) s =
    let m = Regex.Match(s, @"^[A-Z](d|[A-Z]){1,3} d[A-Z]{2}$")
    if m.Success then
        Some s
    else
        None
let outerLondonPrefixes =
    ["BR";"CR";"DA";"EN";"HA";"IG";"KT";"RM";"SM";"TW";"UB";"WD"]
let (|OuterLondon|) (s : string) =
    outerLondonPrefixes
    |> List.tryFind (s.StartsWith)
let promotionAvailable (postcode : string) =
    match postcode with
    | PostCode(Some p) & OuterLondon(Some o) ->
        printfn "We can offer the promotion in %s (%s)" p o
    | PostCode(Some p) & OuterLondon(None) ->
        printfn "We cannot offer the promotion in %s" p
    | _ ->
        printfn "Invalid postcode"
let demo() =
    // "We cannot offer the promotion in RG7 1DP"
    "RG7 1DP" |> promotionAvailable
    // "We can offer the promotion in RM3 5NA (RM)"
    "RM3 5NA" |> promotionAvailable
    // "Invalid postcode"
    "Hullo sky" |> promotionAvailable
demo()
Listing 6-30

Using & with Active Patterns

In Listing 6-30, we have two Active Patterns, a PostCode one that validates UK postcodes and an OuterLondon one that checks whether a postcode has one of the defined prefixes (and also returns which prefix matched). In the promotionAvailable function, we use & to match on both PostCode and OuterLondon for the main switching logic.

Note

The symbol to “and” together items in a pattern match is a single &, in contrast to && that is used for logical “and” in, for example, if expressions.

Incidentally, it might look as though PostCode and OuterLondon would each be called twice for each input string, but this is not the case. The code is more efficient than it appears at first glance.

Pattern Matching on Types

Occasionally, even functional programmers have to deal with type hierarchies! Sometimes, it’s because we are interacting with external libraries like System.Windows.Forms, which make extensive use of inheritance. Sometimes, it’s because inheritance is genuinely the best way to model something, even in F#. Whatever the reason, this can place us in a position where we need to detect whether an instance is of a particular type or is of a descendent of that type. You won’t be surprised to learn that F# achieves this using pattern matching.

In Listing 6-31, we define a two-level hierarchy with a top-level type of Person and one lower-level type Child, which inherits from Person and adds some extra functionality, in this case, just the ability to print the parent’s name. (For simplicity, I’m assuming one parent per person.)
type Person (name : string) =
    member _.Name = name
type Child(name, parent : Person) =
    inherit Person(name)
    member _.ParentName =
        parent.Name
let alice = Person("Alice")
let bob = Child("Bob", alice)
let people = [ alice; bob :> Person ]
// Person: Alice
// Child: Bob of parent Alice
people
|> List.iter (fun person ->
    match person with
    | :? Child as child ->
        printfn "Child: %s of parent %s" child.Name child.ParentName
    | _ as person ->
        printfn "Person: %s" person.Name)
Listing 6-31

Pattern matching on type

With this little hierarchy in place, we define a list called people and put both alice and bob into the list. Because collections require elements to all be the same type, we must shoehorn (upcast) bob back into a plain old Person. Then when we iterate over the list, we must use pattern matching to identify whether each element is “really” a Child, using the :? operator, or is just a Person. I use a wildcard pattern “_” to cover the Person case; otherwise, I will get a compiler warning. This is because the operation “:? Person” is redundant, since all the elements are of type Person.

Pattern matching on types is indispensable when dealing with type hierarchies in F#, and I use it unhesitatingly when hierarchies crop up.

Pattern Matching on Null

Remember back in Chapter 3 we used Option.ofObj and Option.defaultValue to process a nullable string parameter? Listing 6-32 shows an example of that approach.
let myApiFunction (stringParam : string) =
    let s =
        stringParam
        |> Option.ofObj
        |> Option.defaultValue "(none)"
    // You can do things here knowing that s isn't null
    sprintf "%s" (s.ToUpper())
// AN ACTUAL STRING, (NONE)
myApiFunction "hello", myApiFunction nullAn actual string
Listing 6-32

Using Option.ofObj

Well there is an alternative, because you can pattern match on the literal null. Here’s Listing 6-32, redone using null pattern matching (Listing 6-33).
let myApiFunction (stringParam : string) =
    match stringParam with
    | null -> "(NONE)"
    | _ -> stringParam.ToUpper()
// AN ACTUAL STRING, (NONE)
myApiFunction "An actual string", myApiFunction null
Listing 6-33

Pattern matching on null

How do you choose between these alternatives? On stylistic grounds, I prefer the original version (Listing 6-32) because – at least for code that is going to be maintained by people with strong F# skills – sticking everywhere to functions from the Option module feels more consistent. But there’s no doubt that the new version (Listing 6-33) is slightly more concise and more likely to be readable to maintainers who are earlier in their F# journey. You might also want to experiment with performance in your use case, since it looks as though the null-matching version creates no intermediate values and may therefore allocate/deallocate less memory. In performance-critical code, this could make quite a difference.

Recommendations

Get used to pattern matching almost everywhere in your code. To help you remember the breadth of its applicability, here’s a table both to remind you of what pattern matching features are available and to help you decide when to use them (Table 6-2).
Table 6-2

Pattern Matching Features and When to Use Them

Feature

Example

Suggested Usage

Match keyword

match x with

| Case payload -> code...

Use widely. Consider Option module (e.g., Option.map) when dealing with option types

Default case (“wildcard”)

match x with

| Case payload -> code...

| _ -> code ...

With caution. Could this cause you to ignore important cases added in the future?

When guards

| x when x < 12 -> code...

Use freely when applicable. Complicated schemes of when-guarding may indicate another approach is needed, for example, Active Patterns

On arrays

match arr with

| [||] -> code...

| [|x|] -> code...

| [|x;y|] -> code...

With caution. The cases can never be exhaustive, so there will always be a wildcard (default) case. Would lists and the cons operator :: be a better fit?

On lists

match l with

| [] -> code...

| acc::elem -> code...

Use freely when applicable. Indispensable in recursive list-processing code

In let bindings on tuples

let a, b = GetNameVersion(...)

Use widely

On records

match song with

| { Title = title } -> code...

Use freely when collaborators are reasonably skilled in F#

On Discriminated Unions with match keyword

match shape with

| Circle r -> code...

Use widely

On DUs using payload item labels

match reading with

| Economy7(Day=day; Night=night) -> code...

Use where it improves readability or avoids mixing elements up

On records in parameter declarations

let formatMenuItem ( { Title = title; Artist = artist } ) = code...

With caution. May be confusing if collaborators are not highly skilled

On Single-Case Discriminated Unions in parameter declarations

let add (Complex(Real=a;Imaginary=b)) (Complex(Real=c;Imaginary=d)) = code...

With caution. May be confusing if collaborators are not highly skilled. Need to be sure the DU will remain single case or at worst that all cases are handled. Very useful in specialized situations

In let bindings on Discriminated Unions

let (Complex(real, imaginary)) = c1

With caution. May be confusing if collaborators are not highly skilled

In Loops and Lambdas

for (name, count) in fruits do

code...

Use freely when applicable, especially on tuples

On Enums

match fileMode with

| FileMode.Read -> "can"

| FileMode.Write -> "cannot"

| ...

With caution. The matching can never be exhaustive unless there is a wildcard case, so new cases added later can cause bugs

Active Patterns

let (|PostalCode|) pattern s = code...

Use where applicable and collaborators are reasonably skilled in F#. Beware of the limitation of seven cases

On Types

match person with

| :? Child as child -> code...

Use freely when forced to deal with OO inheritance

On Null

match stringParam with

| null -> code...

Use freely, but also consider mapping to an option type and using, for example, Option.map and Option.bind

Summary

If you aren’t pattern matching heavily, you aren’t writing good F# code. Remember that you can pattern match explicitly using the match keyword , but you can also pattern match in let bindings, loops, lambdas, and function declarations. Active Patterns add a whole new layer of power, letting you map from somewhat open-ended data like strings or floating-point values to much more strongly typed classifications.

But pattern matching can be overdone, leading to code that is unreadable to collaborators who may not be experienced in F#. Doing this violates the principle of motivational transparency.

In the next chapter, we’ll look more closely at F#’s primary mechanism for storing groups of labeled values: record types.

Exercises

Exercise 6-1 – Pattern Matching On Records With Dus
Exercise: Let’s say you want to amend the code from Listing 6-12 so that a meter reading can have a date. This is the structure you might come up with:
type MeterValue =
| Standard of int
| Economy7 of Day:int * Night:int
type MeterReading =
    { ReadingDate : DateTime
      MeterValue : MeterValue }
How would you amend the body of the MeterReading.format function so that it formats your new MeterReading type in the following form?
"Your readings on: 01/01/2022: Day: 0003432 Night: 0098218"
"Your reading on: 01/01/2022 was 0012982"
You can use DateTime.ToShortDateString() to format the date.
module MeterReading =
    let format(reading : MeterReading) =
        raise <| System.NotImplementedException()
        "TODO"
let reading1 = { ReadingDate = DateTime(2022, 01, 01)
                 MeterValue = Standard 12982 }
let reading2 = { ReadingDate = DateTime(2022, 01, 01)
                 MeterValue = Economy7(Day=3432, Night=98218) }
// "Your readings on: 01/01/2022: Day: 0003432 Night: 0098218",
// "Your reading on: 01/01/2022 was 0012982"
reading1 |> MeterReading.format, reading2 |> MeterReading.format
Exercise 6-2 – Record Pattern Matching and Loops
Exercise: Start with this code from Listing 6-21 :
let fruits =
    [ "Apples", 3
      "Oranges", 4
      "Bananas", 2 ]
// There are 3 Apples
// There are 4 Oranges
// There are 2 Bananas
for (name, count) in fruits do
    printfn "There are %i %s" count name
// There are 3 Apples
// There are 4 Oranges
// There are 2 Bananas
fruits
|> List.iter (fun (name, count) ->
    printfn "There are %i %s" count name)

Add a record type called FruitBatch to the code, using field names Name and Count. How can you alter the fruits binding to create a list of FruitBatch instances and the for loop and iter lambda so that they have the same output as they did before you added the record type?

Exercise 6-3 – Zip+4 Codes and Partial Active Patterns
Exercise: In the United States, postal codes can take the form of simple five-digit Zip codes, or “Zip+4” codes, which have five digits, a hyphen, and then four more digits. Here is some code that defines Active Patterns to identify Zip and Zip+4 codes, but with the body of the Zip+4 pattern omitted. The exercise is to add the body.
open System
open System.Text.RegularExpressions
let zipCodes = [
    "90210"
    "94043"
    "94043-0138"
    "10013"
    "90210-3124"
    // Letter O intead of zero:
    "1OO13" ]
let (|USZipCode|_|) s =
    let m = Regex.Match(s, @"^(d{5})$")
    if m.Success then
        USZipCode s |> Some
    else
        None
let (|USZipPlus4Code|_|) s =
    raise <| NotImplementedException()
zipCodes
|> List.iter (fun z ->
    match z with
    | USZipCode c ->
        printfn "A normal zip code: %s" c
    | USZipPlus4Code(code, suffix) ->
        printfn "A Zip+4 code: prefix %s, suffix %s" code suffix
    | _ as n ->
        printfn "Not a zip code: %s" n)

Hint: A regular expression to match Zip+4 codes is “^(d{5})-(d{4})$”. When this expression matches, you can use m.Groups.[1].Value and m.Groups.[2].Value to pick out the prefix and suffix digits.

Exercise Solutions

This section shows solutions for the exercises in this chapter.

Exercise 6-1 – Pattern Matching on Records With Dus
I tackled this exercise in two passes. In the first pass, I pattern matched on the whole MeterReading structure using a combination of record pattern matching and DU pattern matching to pull out the date and reading or readings:
type MeterValue =
| Standard of int
| Economy7 of Day:int * Night:int
type MeterReading =
    { ReadingDate : DateTime
      MeterValue : MeterValue }
module MeterReading =
    let format(reading : MeterReading) =
        match reading with
        | { ReadingDate = readingDate
            MeterValue = Standard reading } ->
            sprintf "Your reading on: %s was %07i"
                (readingDate.ToShortDateString()) reading
        | { ReadingDate = readingDate
            MeterValue = Economy7(Day=day; Night=night) } ->
            sprintf "Your readings on: %s were Day: %07i Night: %07i"
                (readingDate.ToShortDateString()) day night
let reading1 = { ReadingDate = DateTime(2022, 01, 01)
                 MeterValue = Standard 12982 }
let reading2 = { ReadingDate = DateTime(2022, 01, 01)
                 MeterValue = Economy7(Day=3432, Night=98218) }
// "Your reading on: 01/01/2022 was 0012982"
// "Your readings on: 01/01/2022: Day: 0003432 Night: 0098218",
reading1 |> MeterReading.format, reading2 |> MeterReading.format
The salient lines are these:
            | { ReadingDate = readingDate
                MeterValue = Standard reading }
        ...
            | { ReadingDate = readingDate
                MeterValue = Economy7(Day=day; Night=night) }

Note how the curly braces {...} indicate that we are pattern matching on records, but within this, we also have <DUCase>(Label=value) syntax to decompose the DU field of the record.

This worked, but I wasn’t happy with it because of the repetition of the reading date pattern match and of the date formatting ((readingDate.ToShortDateString())).

In a second pass, I eliminated the repetition. I used pattern matching in the parameter declaration to pick out the date and value fields. I also created a formatted date string in one place rather than two.
module MeterReading =
    // "MeterReading."" prefix only needed in Notebooks where there may be
    // more than one definition of MeterReading in scope.
    let format { MeterReading.ReadingDate = date; MeterReading.MeterValue = meterValue } =
        let dateString = date.ToShortDateString()
        match meterValue with
        | Standard reading ->
            sprintf "Your reading on: %s was %07i"
                dateString reading
        | Economy7(Day=day; Night=night) ->
            sprintf "Your readings on: %s were Day: %07i Night: %07i"
                dateString day night
Exercise 6-2 – Record Pattern Matching and Loops
In the fruits binding, you just need to use standard record-construction syntax:
type FruitBatch = {
    Name : string
    Count : int }
let fruits =
    [ { Name="Apples"; Count=3 }
      { Name="Oranges"; Count=4 }
      { Name="Bananas"; Count=2 } ]
In the for loop and List.iter lambda, you can use record pattern matching in the form { FieldName1=label1; FieldName2=label2...} to recover the name and count values.
// There are 3 Apples
// There are 4 Oranges
// There are 2 Bananas
for { Name=name; Count=count } in fruits do
    printfn "There are %i %s" count name
// There are 3 Apples
// There are 4 Oranges
// There are 2 Bananas
fruits
|> List.iter (fun { Name=name; Count=count } ->
    printfn "There are %i %s" count name)

Remember that in both cases, just using “dot” notation in the loop/lambda body to retrieve the record fields is also legitimate and more common.

Exercise 6-3 – Zip+4 Codes and Partial Active Patterns
The body of the Zip+4 Active Pattern should look something like this:
let (|USZipPlus4Code|_|) s =
    let m = Regex.Match(s, @"^(d{5})-(d{4})$")
    if m.Success then
        USZipPlus4Code(m.Groups.[1].Value,
                       m.Groups.[2].Value)
        |> Some
    else
        None

See how when the regular expression matches, we return a USZipPlus4Code case whose payload is a tuple of the two matching groups.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.174.44