Using Discriminated Union Labelled Fields

A few weeks ago, I re-discovered labelled fields in discriminated unions. Despite the fact that they look like tuples, they are not.

This is my entry to F# Advent Calendar 2021. Thanks to Sergey Tihon for organising the Advent Calendar each year.

A few weeks ago, I re-discovered labelled fields in discriminated unions:

// Without labels
type Customer =
    | Registered of string * string option * bool
    | Guest of string

// With labels
type Customer =
    | Registered of Name:string * Email:string option * IsEligible:bool
    | Guest of Name:string

I knew that the feature existed but I've usually built specific types, generally records, for each union case, so hadn't really used them in anger before. This isn't a new feature: Field labels in discriminated union case members were introduced in F# 3.1. Despite the fact that they look like tuples, they are not. For example, tuples in F# do not support labels like they do in C#.

In this post, we will look at how to make use of this feature.

Getting Started

We are going to start with a simple business feature:

(*
Feature: Applying a discount

Scenario: Eligible Registered Customers get 10% discount 
when they spend £100 or more

Given the following Registered Customers
|Customer Id|Email          |Is Eligible|
|John       |john@test.org  |true       |
|Mary       |mary@test.org  |true       |
|Richard    |               |false      |
|Alison     |alison@test.org|false      |

When  spends 
Then their order total will be 

Examples:
|Customer Id| Spend | Total |
|Mary       |  99.00|  99.00|
|John       | 100.00|  90.00|
|Richard    | 100.00| 100.00|
|Sarah      | 100.00| 100.00|
*)

We are going to create two functions: One to calculate the totals after discount and one to return the email address of eligible customers. Emails are mandatory for Eligible customers and optional for Registered customers.

Creating Labelled Fields

The type design used in this post is specifically designed for the task of discovering how we can work with labelled fields. We start with a simple discriminated union with two union cases:

type Customer =
    | Registered of Name:string * Email:string option * IsEligible:bool
    | Guest of Name:string

Pattern Matching on Labelled Fields

As they look like tuples, can we deconstruct them in a match expression in the same way without the labels? It turns out that you can:


 
let calculateOrderTotal customer spend =
    let discount = 
        match customer with
        | Registered (name, email, isEligible) when isEligible && spend >= 100M -> spend * 0.1M 
        | _ -> 0M
    spend - discount

In this case, I'm only interested in the IsEligible flag, so will wildcards work? Yes they do:

let calculateOrderTotal customer spend =
    let discount = 
        match customer with
        | Registered (_, _, isEligible) when isEligible && spend >= 100M -> spend * 0.1M 
        | _ -> 0M
    spend - discount

Now let's try adding the labels in and get the values like we would with fields on a record type. Sadly, this doesn't work as we get a compiler error:

// Compiler Error
let calculateOrderTotal customer spend =
    let discount = 
        match customer with
        | Registered (Name = name, Email = email, IsEligible = isEligible) when isEligible && spend >= 100M -> spend * 0.1M 
        | _ -> 0M
    spend - discount

As I said earlier, they look like tuples but they aren't. The fix turns out to be simple: Replace the comma separators with semi-colons:

let calculateOrderTotal customer spend =
    let discount = 
        match customer with
        | Registered (Name = name; Email = email; IsEligible = isEligible) when isEligible && spend >= 100M -> spend * 0.1M 
        | _ -> 0M
    spend - discount

As we are not using name and email, can we use wildcards to ignore their data? Yes we can:

let calculateOrderTotal customer spend =
    let discount = 
        match customer with
        | Registered (Name = _; Email = _; IsEligible = isEligible) when isEligible && spend >= 100M -> spend * 0.1M 
        | _ -> 0M
    spend - discount

How about wildcards to ignore the fields? This change gives us a compiler error:

// Compiler Error
let calculateOrderTotal customer spend =
    let discount = 
        match customer with
        | Registered (_; _; IsEligible = isEligible) when isEligible && spend >= 100M -> spend * 0.1M 
        | _ -> 0M
    spend - discount

Again, the fix turns out to be simple: Remove the fields completely from the pattern match:

let calculateOrderTotal customer spend =
    let discount = 
        match customer with
        | Registered (IsEligible = isEligible) when isEligible && spend >= 100M -> spend * 0.1M 
        | _ -> 0M
    spend - discount

That's better but it would be nice if we could apply a filter directly rather than having to get the value and then test it. We can do this with records and thankfully it is available here too:

let calculateOrderTotal customer spend =
    let discount = 
        match customer with
        | Registered (IsEligible = true) when spend >= 100M -> spend * 0.1M 
        | _ -> 0M
    spend - discount

We can also combine the filter and the value getter as shown in the following function where we filter on IsEligible and return the value of the Email field into a local binding:

let tryGetEligibleEmail customer =
    match customer with
    | Registered (IsEligible = true; Email = email) -> Some email 
    | _ -> None

In summary, we use ',' for separating the fields when we don't use the labels in the pattern match and ';' when we do. If we are not interested in a field, don't use it in the match. We can use filters and value getters in the same match.

Creating an Instance of a DU Case

You can create an instance of a union case without specifying the field labels:

// let john = Registered ( "John", Some "john@test.org", true )

Personally, I think it makes more sense to use the labels if you provided them in the first place:

let john = Registered ( Name = "John", Email = Some "john@test.org", IsEligible = true )
let mary = Registered ( Name = "Mary", Email = Some "mary@test.org", IsEligible = true )
let richard = Registered ( Name = "Richard", Email = None, IsEligible = false )
let alison = Registered ( Name = "Alison", Email = Some "alison@test.org", IsEligible = false )
let sarah = Guest ( Name = "Sarah" )

Verifying these the functions with the instances is trivial. Firstly, the calculateOrderTotal function:

let assertJohn = calculateOrderTotal john 100.0M = 90.0M
let assertMary = calculateOrderTotal mary 99.0M = 99.0M
let assertRichard = calculateOrderTotal richard 100.0M = 100.0M
let assertSarah = calculateOrderTotal sarah 100.0M = 100.0M

and then the tryGetEligibleEmail function:

let assertMaryEmail = tryGetEligibleEmail mary = Some "mary@test.org"
let assertRichardEmail = tryGetEligibleEmail richard = None
let assertAlisonEmail = tryGetEligibleEmail alison = None
let assertSarahEmail = tryGetEligibleEmail sarah = None

What Happens If ...

What happens if you decide not to include a label for the Name field?

type Customer =
    | Registered of string * Email:string option * IsEligible:bool
    | Guest of Name:string

The tuple-style pattern match with no labels works fine as does the version with the wildcards:

let calculateOrderTotal customer spend =
    let discount = 
        match customer with
        | Registered (name, email, isEligible) when isEligible && spend >= 100M -> spend * 0.1M 
        | _ -> 0M
    spend - discount

let calculateOrderTotal customer spend =
    let discount = 
        match customer with
        | Registered (_, _, isEligible) when isEligible && spend >= 100M -> spend * 0.1M 
        | _ -> 0M
    spend - discount

Removing the label from the original version with labels causes a compiler error:

// Compiler Error
let calculateOrderTotal customer spend =
    let discount = 
        match customer with
        | Registered (name; Email = email; IsEligible = true) when spend >= 100M -> spend * 0.1M 
        | _ -> 0M
    spend - discount

Removing that field and only using labelled fields works correctly:

let calculateOrderTotal customer spend =
    let discount = 
        match customer with
        | Registered (Email = email; IsEligible = true) when spend >= 100M -> spend * 0.1M 
        | _ -> 0M
    spend - discount

You don't have to supply every field with a label if you aren't going to pattern match on it with that label. I like consistency and would either supply labels to all fields or none at all.

Summary

I hope that you found this short post useful. Even if you decide not to use these features, it is still nice to know that they are available to you.
I have written an ebook called Essential Functional-First F#. All of the royalties go to the F# Software Foundation to support their promotion of the F# language and community around the world.

Follow me on Twitter at @ijrussell!

Blog 8/10/23

Machine Learning Pipelines

In this first part, we explain the basics of machine learning pipelines and showcase what they could look like in simple form. Learn about the differences between software development and machine learning as well as which common problems you can tackle with them.

Blog

Functional Validation in F# Using Applicatives

Learn how to implement functional validation in F# using applicatives. Handle multiple errors elegantly with Railway-Oriented Programming and concise functional patterns.

Blog

Using GCP Cloud Functions with F#

Learn how to build and test Google Cloud Functions in F#, using dependency injection, configuration, and pub/sub messaging for real-world cloud apps.

Blog

Celebrating Homai - Using AI for Good

Our colleague Aigiz Kunafin has achieved an outstanding milestone - importance of his side-project Homai was acknowledged by the “AI for Good” Initiative of United Nations.

Blog 3/17/22

Using NLP libraries for post-processing

Learn how to analyse sticky notes in miro from event stormings and how this analysis can be carried out with the help of the spaCy library.

Blog

Using Historical Data to Simulate Truck Journey

Discover how historical truck data and Python simulations can predict journey times and CO₂ emissions, helping logistics become smarter and greener.

Blog 5/1/21

Ways of Creating Single Case Discriminated Unions in F#

There are quite a few ways of creating single case discriminated unions in F# and this makes them popular for wrapping primitives. In this post, I will go through a number of the approaches that I have seen.

Blog 6/24/21

Using a Skill/Will matrix for personal career development

Discover how a Skill/Will Matrix helps employees identify strengths and areas for growth, boosting personal and professional development.

Unternehmen 1/19/22

PKS

PKS is a team of experienced software analysts, programming experts and web developers. Since 1991, they have been planning innovative, low-risk and future-proof software transformations worldwide.

News 9/29/22

TIMETOACT GROUP acquires monitoring specialist OpenAdvice

We expand our offer in application performance monitoring and business service assurance with acquisition of OpenAdvice IT Services GmbH

Standort

Location in Ravensburg

Find PKS Software GmbH in Ravensburg: Georgstraße 15; 88214 Ravensburg; Tel.: +49 751 56140 0; Mail: info@pks.de

Kompetenz

Green IT: Your Status Quo Assessment for Sustainable IT

Sustainability in IT means using resources efficiently, reducing energy consumption, maximizing the lifespan of devices, and minimizing environmental impact at the end of their lifecycle. These actions help improve a company's carbon footprint, minimize compliance risks, and reduce costs in the long run.

News 7/6/23

TIMETOACT GROUP acquires STAGIL

With acquiring STAGIL, TIMETOACT GROUP consolidate its position as one of the leading Atlassian partners globally.

Schild als Symbol für innere und äußere Sicherheit
Branche

Internal and external security

Defense forces and police must protect citizens and the state from ever new threats. Modern IT & software solutions support them in this task.

Unternehmen 1/19/23

Sustainability in the TIMETOACT GROUP

Sustainability is one of the big topics of our time and we also want to get involved and face up to our responsibility as TIMETOACT GROUP. Find out everything about our sustainability activities here.

Digitaler LKW auf vernetzter Straße – smarte Logistik und Transportlösungen.
Branche

Transport & Logistics

Digital solutions for networked transport and logistics processes

Standort

Location in Walldorf

Find Walldorf Consulting AG locally in Walldorf: Altrottstraße 31; 69190 Walldorf, Germany; Tel.: +49 6227 7326-40; Mail: info@walldorfconsulting.com

Standort

Site Singapore

Walldorf Consulting Asia Pacific Pte. Ltd; 3 Harbourfront Place; #11-01, Harbourfront Tower 2; Singapore 099254; Phone: +65 6350 5603 E-Mail: info@walldorfconsulting.com

News 3/23/23

TIMETOACT GROUP becomes patron of ITAM Forum

As part of the cooperation, TIMETOACT GROUP now also offers companies comprehensive consulting services for IT Asset Management certification ISO/IEC 19770-1

Kompetenz

Governance & Operational Excellence

Digitalization inevitably leads to business processes changing and roles and responsibilities being redistributed. At the same time, new legal requirements, regulations and standards need to be met.

Bleiben Sie mit dem TIMETOACT GROUP Newsletter auf dem Laufenden!