jonnifyfsharp

Dive C1.06.2

Discriminated unions

A value is case A or case B — one of a closed set, each carrying its own data.

A discriminated union is F#'s sum type. The declaration type Shape = Circle of float | Rectangle of float * float names a type whose values are one of a fixed set of cases. Each case is a distinct shape, and each may carry data — a Circle carries one float, a Rectangle carries two. A value is built by naming a case and supplying its data, and it is taken apart by match, which binds the carried data per case. Because the set of cases is closed, the compiler checks exhaustiveness: leave a case out of a match and it warns. Unions may be recursive — a tree references itself — or hold a single case to give a primitive a domain name.

C1.06.2 · part 1

A closed set of cases

Each line after the | is a case. A case with no of carries nothing; Circle of float carries one value; Rectangle of float * float carries a pair. To build a value, write the case name and its data: Circle 1.0, Rectangle (3.0, 4.0).

type Shape =
    | Circle of float
    | Rectangle of float * float

let c = Circle 1.0
let r = Rectangle (3.0, 4.0)
// both are values of type Shape

A match takes the value apart. One branch per case names the case and binds the data it carries; the body computes from those bindings. The compiler reads the case list off the type and confirms every case is handled.

let area s =
    match s with
    | Circle r -> System.Math.PI * r * r
    | Rectangle (w, h) -> w * h
// val area : Shape -> float

area (Rectangle (3.0, 4.0))
// val it : float = 12.0
discriminated union
a sum type: a value is exactly one of a closed set of named cases.
case
one alternative — a name, optionally with of and the data it carries.
match
takes a value apart by case, binding the carried data in each branch.
exhaustiveness
the compiler warns if a match omits a case the type allows.
C1.06.2 · part 2

One value, one matched case

Pick a Shape value below. The diagram lights the case the match selects, and the readout shows which branch ran and the float it returned. Every number is the value area computes — the same one FSI prints.

match s with · the matched case lights up

Circle r area = π r² one float Rectangle (w, h) area = w · h a pair of floats
area (Rectangle (3.0, 4.0)) · matched Rectangle (w, h) · w * h = 3.0 * 4.0 => val it : float = 12.0
valuematched casearea
Circle 1.0Circle r3.141592654
Circle 2.0Circle r12.56637061
Rectangle (3.0, 4.0)Rectangle (w, h)12.0

The value names its case; the match picks the matching branch and binds the data it carries. Add a case to the type and the compiler points at every match that has not handled it yet.

Sum type

A value is case A or case B or case C — one of a closed set, each carrying its own data.

F# union

Circle of float | Rectangle of float * float — matched exhaustively, the data bound per case.

C1.06.2 · part 3

Recursive and single-case unions

A case may carry the union's own type, which makes the union recursive. A binary tree is the classic shape: a Node holds a left subtree, a value, and a right subtree, and a Leaf ends a branch. A function over the tree matches the two cases and recurses on the subtrees.

type Tree =
    | Leaf
    | Node of Tree * int * Tree    // a case carries Tree itself

let rec sumTree t =
    match t with
    | Leaf -> 0
    | Node (l, v, r) -> sumTree l + v + sumTree r

A union with one case wraps a primitive to give it a domain type. A bare string says nothing about what the string is; Email below is a distinct type, so a function that takes an email cannot be handed a plain string by mistake.

type Email = Email of string

let e = Email "alex@example.com"
// val it : Email = Email "alex@example.com"
Named fields. A case's data can be named for readability: Circle of radius: float. The name documents the field and can be used at the call site (Circle(radius = 1.0)), without changing the case's shape.

Where this leads

Records are the and; unions are the or. The next dive composes them for a purpose: when a union lists only the combinations a domain allows, the values it admits and the values the domain allows are the same set — so the wrong value cannot be written, and the compiler keeps the rule.

References

Sources

Related in this course