Ways of Creating Single Case Discriminated Unions in F#

In this post, I will go through a number of the approaches that I have seen 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 primatives. In this post, I will go through a number of the approaches that I have seen. The inspiration for this post is Twitter threads from the likes of @McCrews, @asp_net and @jordan_n_marr. It is in no way exhaustive but should give you plenty of ideas.

Basics

We start with a simple single case discriminated union:

type CustomerId = CustomerId of Guid

To create a CustomerId and deconstruct it to get the value, we can do the following:

let id = CustomerId (Guid.NewGuid())
let (CustomerId value) = id

We can add a private modifier to it which means that we do not have access to the constructor and cannot create a CustomerId outside of the module the type is defined in:

type CustomerId = private CustomerId of Guid

// Problem outside of current module
let id = CustomerId (Guid.NewGuid())
let (CustomerId value) = id

This is easy to solve in a number of ways, firstly using a module.

Using a Module

If we create a module with the same name as the type definition, we can add functions to create and extract the value:

type CustomerId = private CustomerId of Guid

module CustomerId =
    let New() = CustomerId (Guid.NewGuid())
    let Value (CustomerId value) = id

We can now create a CustomerId and extract the value from it like this:

let id = CustomerId.New()
let value = CustomerId.Value id

It's also possible to do this without the need for a module.

Without Using a Module

We can add an instance member for extracting the value and a static member to create an instance of CustomerId:

type CustomerId = private CustomerId of Guid
    with
        member this.Value =
            let (CustomerId value) = this
            value
        static member New() = CustomerId (Guid.NewGuid())

This now makes extracting the value much cleaner:

let id = CustomerId.New()
let value = id.Value

If you think that the two lines for the Value function are one too many, we can write it in one line. This is the first version using the in keyword:

type CustomerId = private CustomerId of Guid
    with
        member this.Value = let (CustomerId value) = this in value
        static member New() = CustomerId (Guid.NewGuid())

It doesn't impact how we create and extract the data:

let id = CustomerId.New()
let value = id.Value

Another way is to use an anonymous function:

type CustomerId = private CustomerId of Guid
    with
        member this.Value = this |> fun (CustomerId value) -> value
        static member New() = CustomerId (Guid.NewGuid())

Again the usage is the same as before:

let id = CustomerId.New()
let value = id.Value

Having a separate New function means that we could add logic to ensure that it cannot be created with an invalid value. If you don't need that, we can remove the private access modifier and the New function:

type CustomerId = CustomerId of Guid with member this.Value = this |> fun (CustomerId value) -> value

Usage then looks like this:

let id = CustomerId (Guid.NewGuid())
let value = id.Value

Alternate Solution

After I'd posted this article, @Savlambda suggested an alternate approach using an active pattern:

module Identifier =
    type CustomerId = private CustomerId of Guid
        with
            static member New() = CustomerId (Guid.NewGuid())

    let (|CustomerId|) (CustomerId value) = value

Using the new module looks like this:

module OtherModule =
    open Identifier

    let id = CustomerId.New()
    let (CustomerId value) = id

So we have quite a few styles and I'm not going to suggest the superiority of one over the others. In one of the Twitter threads that led to this post, @pblasucci said that you can do the same with records as well!
 

Using a Record Type

Let's create a simple record type:

type CustomerId = { CustomerId : Guid }

Creation and value extraction look like this:

let id = { CustomerId = Guid.NewGuid() }
let value = id.CustomerId

That's not too different! We can even add member functions to record types:

type CustomerId = { CustomerId : Guid }
    with 
        member this.Value = this.CustomerId
        static member New() = { CustomerId = Guid.NewGuid() }

This means that we can create and extract the value in exactly the same way that we did with single case discriminated unions:

let id = CustomerId.New()
let value = id.Value

I don't know what impact using records has on performance, maybe that will be another post?

Summary

In this post, we have seen a few styles of single case discriminated unions and records to wrap up primatives. It is pretty trivial to use any of the approaches suggested in this artlcle and will give you additional type safety over that given by a raw staticaly typed primative, particularly when you have rules that define the type.
As always, let me know what you think about this post or any suggestions about future posts. https://twitter.com/ijrussell

Blog 12/3/21

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.

Process Integration & Automation
Service

Process Integration & Automation

Digitizing and improving business processes and reacting to changes in an agile way – these are the challenges that more and more companies need to face.

Security, Identity & Access Management
Service

Security, Identity & Access Management

Time and again we hear about hacker attacks on companies that target sensitive company data. Therefore, security and access control of data must never be neglected.

Managed Services & Managed Support
Service

Managed Services & Managed Support

Our Managed Service Team of specialists will relieve your IT department. We ensure that you can work more efficiently, reliably and quickly

Digital Workplace & Employee Experience
Service

Digital Workplace & Employee Experience

The Digital Workplace gained in importance, especially in recent months, becoming indispensable for many companies. The Microsoft Office 365 platform provides an ideal basis for this development.

Unternehmen

ARS Computer und Consulting GmbH

ARS is one of the leading companies in Software Engineering. For them, Cognitive Solutions and Artificial Intelligence are the future.

News 5/7/21

Equistone acquires majority stake in TIMETOACT GROUP

TIMETOACT GROUP's already successful buy-&-build strategy will be boosted with both know-how and capital

Unternehmen

novacapta

Based on Microsoft SharePoint, Office 365, Azure, BizTalk and PowerBI novaCapta realizes intranets, collaboration portals, business intelligence solutions, individual applications and more.

Google Logo
Technologie 6/29/20

Google

Google is more than Google Search and Google Ads! We advise you on Google Analytics, Google Cloud Platform, G Suite, Google Cloud IoT and more!

Logo RedHat
Technologie 7/2/20

RedHat

We are RedHat Advanced Partner. With RedHat as the market leader in Open Source IT solutions, we support our customers in actively designing and implementing their cloud journey.

Unternehmen

Directions to TIMETOACT GROUP in Cologne

Whether you travel by car, train or plane, we will show you the best way to get to the Mediaparkt in Cologne.

Headerbild zur AI Factory for Insurance
Service 7/5/21

AI Factory for Insurance

The AI Factory for Insurance is an innovative organisational model combined with a flexible, modular IT architecture. It is an innovation and implementation factory to systematically develop, train and deploy AI models in digital business processes.

Unternehmen 9/16/20

synaigy

synaigy – the Digital Agency for your strategic project in digital customer dialogue offers suitable solutions in all relevant areas of Digital Customer Engagement.

Headerbild zur offenen und sicheren IT bei Versicherungen
Service

Open and secure IT

Just a few years ago, insurers were reluctant to move into the cloud or platform world. Concerns about security and governance often prevailed. The paradigm has changed.

Standort

Location in Augsburg

Find novaCapta GmbH in Augsburg: Schertlinstraße 19, 86159 Augsburg, +49 821 789 887 90, info.augsburg@novacapta.de

Branche 2/20/25

Insurance

Insurance companies live by making a promise to people - and that promise is security.

Standort

Location in Vienna

Find the catworkx GmbH, CLOUDPILOTS Software & Consulting GmbH, IPG Information Process Group Austria GmbH and TIMETOACT GROUP Österreich GmbH in Vienna

Headerbild zu Digitalem Ökosystem
Service

Fit for the digital ecosystem

Insurers are digitally networking with their ecosystem to gain critical capabilities in a division of labor. Personal data, object data are securely exchanged via common digital interfaces.

Standort

Location in Seligenstadt

Find novaCapta GmbH and TIMETOACT Software & Consulting GmbH in Seligenstadt: Dr.-Hermann-Neubauer-Ring 40; 63500 Seligenstadt

News

Proof-of-Value Workshop

Today's businesses need data integration solutions that offer open, reusable standards and a complete, innovative portfolio of data capabilities. Apply for one of our free workshops!

Bleiben Sie mit dem TIMETOACT GROUP Newsletter auf dem Laufenden!