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
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.
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.
From a class hierarchy to a DU
One type Shape = Circle | Rectangle | Triangle replaces the base class and its subclasses — closed, and in one place.
- the datahow a closed sum replaces an open hierarchy
From virtual dispatch to exhaustive match
One area function matches every case, and the compiler warns FS0025 when a case is missed.
- the dispatchexhaustiveness, and the expression-problem trade
From the Visitor pattern to a recursive DU
A recursive Expr union read by a recursive eval — no IVisitor, no Accept, no double dispatch.
- the treea tree of node types as data, the fold as a function
References
Sources
- Microsoft — Discriminated unions — the case declaration, named fields, and recursive unions in F#.
- Microsoft — Pattern matching —
matchover a union and the exhaustiveness check. - Microsoft — Inheritance (C#) — abstract classes,
override, and virtual dispatch, the habit this module translates.
Related in this course
- C2.04.1 — From a class hierarchy to a DU — the closed sum, case by case.
- C1.06 — Discriminated unions & records — the same union type, in language depth.
- C2.01 — From classes to modules — data beside behaviour, the move this module continues.