Tuples and shapes of your own
Grouping values that belong together, taking a pair apart in a let, and a variant type that lists every shape a value may take.
A tuple is a fixed group
A tuple groups values that belong together, written between brackets and separated by commas: (3, "apples"). fst and snd read the two halves of a pair, and a pattern reads a whole tuple at once, so let (a, b) = (2, 5) names both parts in one go. The brackets are part of the syntax, not a function call, which is why fst and snd exist rather than a general index.
The number of items and their types are part of the tuple type, which makes a tuple a grouping rather than a collection. A list can grow and shrink at run time; a tuple cannot, so it is the right shape when two values are always two.
A variant lists the possibilities
A variant type says that a value is one of several named shapes. type shape = Circle of float | Square of float says that a shape is either a circle carrying a radius or a square carrying a side, and nothing else — the two constructors are the only ways to build one.
Each constructor is also a function from its payload to the type, so Circle 2.0 is how a value is made, and match is how it is taken apart. Putting the type and the match side by side is the OCaml habit: the compiler then knows every shape that exists, and warns when a function forgets one.
Named shapes, unnamed groups
A variant is a type with a fixed list of constructors. Each constructor can carry nothing, one value, or several values between brackets; the declaration says exactly what each one carries. That is why a value of the variant type can be taken apart only by a match, and why the compiler can tell you which constructor a branch forgot.
A tuple is the unnamed alternative: (3, "apples") has the type int * string, and the order and the types are the whole identity of the value. A variant is the named alternative: Circle 2.0 and Square 2.0 can both be a shape even though they carry the same kind of payload, because the constructor records which case it is.
Use a tuple when the values are always together and mean the same thing in every context. Use a variant when one value can take one of several forms and the code has to choose. Mixing them is normal: a constructor can carry a tuple, as in Rectangle of float * float, and the pattern then names both halves.
How a variant value is built and taken apart
Circle 2.0 is a constructor applied to a payload, and the result has type shape; the constructor is the only way to make a value of that case. The compiler stores the tag — which case it is — alongside the payload, and match reads the tag before it binds anything. That is why a pattern for Circle r cannot accidentally match a Square s: the tags differ.
This is also why adding a constructor is a compile-time event. The type has one more case, every match that claims to cover it is now incomplete, and the warning points at the match rather than at the value. The compiler is doing the work a visitor pattern does in a language without variants.
What goes wrong
Two variants in one file cannot share a constructor name. Defining type colour = Red | Green and type light = Red | Yellow in the same module is an error, because Red would be ambiguous at every use. Rename one, or put the types in separate modules, which is the usual answer in a larger program.
The other is a pattern that names a constructor without its payload: | Circle -> ... for a Circle of float is a type error, because the pattern has the wrong shape. Write | Circle r -> ... and use r, or | Circle _ -> ... when the payload is not needed.
Three things to watch: constructor names are capitalised and share one namespace per file, so two variants cannot both define Circle; a variant type name is lowercase, and writing type Shape = ... is a syntax error; and compare on a variant orders by constructor declaration rather than by meaning, so Circle 5.0 < Square 1.0 compiles but probably does not say what you meant.
Try these next
- Add a
Triangle of float * float * floatconstructor and a branch that answers its perimeter. - Write a variant
day = Mon | Tue | ...and a function that groups the days into weekdays and weekend. - Put a tuple inside a constructor and take it apart in one pattern.
A variant with and without a payload
type pet = Cat | Dog of string
let noise pet =
match pet with
| Cat -> "meow"
| Dog name -> name ^ " barks"
let () = print_endline (noise (Dog "Rex"))A pair and two shapes
Run it and read the five lines. Then add a `Rectangle` constructor carrying two floats, a width and a height, handle it in `area`, and print one.
A drink, a size and a count
Run it and read the lines. Then add a `Juice of string` constructor, give it a price, and handle it in both functions.
A constructor name starts with a capital letter, and two variants in one file cannot reuse it. The variant is what makes a match exhaustive: add a third shape and every function that forgot it becomes a warning rather than a bug found later.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does let (a, b) = (2, 5) do?
