jonnifyfsharp

Module C2.04

From inheritance to discriminated unions

A closed set of named cases, read by one match the compiler keeps honest.

A C# developer models "one of several shapes" with an abstract base class, a subclass per shape, and virtual dispatch — each subclass overrides Area() with its own formula. F# models the same domain with a discriminated union: a single type that lists every case in one place (Circle, Rectangle, Triangle), and a single match for each operation. The payoff is the thing inheritance cannot give: the compiler checks exhaustiveness. Add a case to the union and every non-total match in the program warns FS0025 — the places that must change announce themselves. Data and behaviour separate too: the DU holds the shapes, free functions hold the operations, so a new operation is a new function, not an edit to every subclass. This module covers all three shifts in three dives.

One type, listing every case

An abstract base class is an open hierarchy: any file can add a subclass, and the set of shapes is never written down in one place. A discriminated union is a closed sum: every case sits in the one declaration, so the complete set is available to the compiler, which can then check that each match covers it. The three dives below take the C# habit apart one piece at a time.

The first dive replaces the class hierarchy itself: the abstract class Shape with its CircleC : Shape and RectC : Shape subclasses becomes one type Shape = Circle of float | Rectangle of float * float | Triangle of float * float. The second replaces virtual dispatch: an operation that C# scatters across subclasses as override methods becomes one function that matches every case — and the compiler enforces that the match is total. The third collapses the Visitor pattern: a recursive tree of node types, which C# walks with an IVisitor and double dispatch, becomes a recursive DU read by a recursive match.

The three dives · select one

C2.04.1 hierarchy → DU C2.04.2 dispatch → match C2.04.3 Visitor → recursive DU the data the dispatch the tree
C2.04.1 · From a class hierarchy to a DU

An abstract base class with one subclass per shape becomes one discriminated union that lists every case in a single declaration — closed, and in one place.

You learn how a closed sum replaces an open hierarchy

Open this dive · or pick another above

The hierarchy is open and scattered; the union is closed and in one place. The compiler can only check the cases it can see, and a DU shows it all of them.

The habit, in miniature

The shift is concrete. Where C# declares an abstract class Shape and puts an Area() override inside each subclass, F# declares one Shape union and one area function that matches every case. The C# below compiles and runs; the F# beside it is its idiomatic translation, with the // val it : echoes produced by dotnet fsi.

C# — an abstract base class and a subclass per shape

abstract class Shape { public abstract double Area(); }
class CircleC : Shape { public double R; public override double Area() => Math.PI*R*R; }
class RectC   : Shape { public double W, H; public override double Area() => W*H; }
// the set of shapes is open: any file may add another subclass

F# — one union, one match

type Shape =
    | Circle of float
    | Rectangle of float * float
    | Triangle of float * float          // a closed set of cases
let area s =                             // val area : Shape -> float
    match s with
    | Circle r -> System.Math.PI * r * r
    | Rectangle (w, h) -> w * h
    | Triangle (b, h) -> 0.5 * b * h
area (Rectangle (2.0, 3.0))              // val it : float = 6.0

C# habit

An abstract class plus a subclass per case, each carrying its own override — open, and spread across files.

F# form

One discriminated union listing every case, read by a total match — closed, in one place, and checked for exhaustiveness.

The three dives

Read them in order. Each stands on valid F# beside the idiomatic C# it replaces: every // val it : echo below was produced by dotnet fsi, and every C# line compiles. Together they take a C# developer from a base class and override to a discriminated union and match, and on to the recursive case the Visitor pattern was built to handle.

live module hub built dive ready soon specified

References

Sources

Related in this course