Discriminated unions and match
A union says a value is one of a fixed set of cases, each carrying its own data. `match` takes it apart, and the compiler knows when a case has been forgotten.
One of several shapes
A discriminated union says that a value is one of a fixed set of cases, and each case may carry data: type Shape = | Circle of int | Rect of int * int. A circle carries one number, a rectangle two. The cases are the only possibilities there are.
This is not a class hierarchy. There is no base type to cast to and no method to override: a Shape is a Shape, and the only way to reach the number inside is to match on the case that carries it. That sounds restrictive until you notice what it removes — the field that is only sometimes filled in, the type tag that can disagree with the fields, the cast that can fail.
match, with every case accounted for
match shape with | Circle r -> ... | Rect (w, h) -> ... takes the value apart and binds whatever each case carries. One branch per case and the match is complete; leave one out and the compiler warns, naming the case you missed. Add a fifth case to a union and every match that ignores it starts warning, so the code tells you where the new possibility has to be handled.
A function per question, each one a match, is the shape of this style: area answers a number, name answers a string, and both are total — every shape has an area and a name, and no branch has to answer "not applicable". A case that carries no data is written without of, and its pattern is just the name.
A final _ branch makes a match complete and also silences the warning that would have told you a case was added. Use it when the remaining cases really share one answer, and leave it out when each case deserves its own branch; the warning is the compiler doing the work of finding every place a new possibility has to be considered.
A union, and two total functions over it
type Shape =
| Circle of int
| Rect of int * int
let area shape =
match shape with
| Circle r -> 3 * r * r
| Rect (w, h) -> w * hThree shapes, two questions
Run it and check the three areas by hand. Then add a `Tri of int * int` case and give `area` and `name` a branch for it, so that every case is handled again.
A union and a match are the alternative to a flag: instead of a kind field and three fields that are empty unless the kind says otherwise, each case carries exactly what it needs and nothing else exists. When a new case appears, the warnings list the functions that must learn about it.
A case can hold the same type again
A union may refer to itself. type Tree = | Leaf of int | Node of Tree * Tree says a tree is either a leaf holding a number or a node holding two smaller trees. The definition is finite even though the values are not, because every recursive step has to end at a leaf.
Functions over such a value are recursive in the same shape. The leaf branch is the base case and returns without calling again; the node branch calls the function on both children and combines the answers. The structure of the type tells you how many recursive calls the function needs, which is why the definition is worth reading before the code.
A case name is also a constructor function. Leaf takes an int and answers a Tree; Node takes a pair of trees. That is why Node (Leaf 3, Leaf 4) builds a value without any new, and why the same names appear in patterns.
The shape rules out the impossible states
The alternative is a tag plus fields that are only meaningful for some tags: a kind string, a radius that is zero for rectangles, a width that is meaningless for circles. Nothing stops the tag and the fields from disagreeing, and every function has to remember which fields are real.
With a union there is no tag to disagree with the data. A Circle carries a radius and nothing else, and the only way to reach it is to match on that case. The compiler then checks every match against every case, so the cost of adding a possibility is paid where it belongs: at the functions that must decide what it means.
Union case names must begin with an uppercase letter, unlike values and functions. A case that carries a tuple is still one payload, so the pattern is Rect (w, h), not Rect w h. And a recursive function needs let rec; without it, the name is not in scope inside its own body.
Try these next
- Add a
Tri of int * intcase to the shape union and let the warnings list every function that needs a branch. - Write
let rec count treethat answers how many leaves a tree has. - Build a tree with four leaves and check its total and depth by hand before running.
A recursive tree, summed and measured
Run it and check both numbers against the sample. Then add a second leaf under the right branch and predict the new total and depth before you run again.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
How much data does the case Rect of int * int carry?
