Chapter 8 — Enums and Pattern Matching
If a struct is “all of these fields at once,” an enum is “exactly one of these possibilities.”
enum Shape {
Circle(radius: float)
Rect(width: float, height: float)
Origin // a variant with no data needs no parens
}
A Shape value is one of those three things: a circle with a radius, a rectangle with a
width and height, or the origin. Each variant can carry its own typed, named fields. You build
one by naming the variant, positionally:
let c = Circle(2.0)
let r = Rect(3.0, 4.0)
let o = Origin
Because the fields are named in the declaration, you can also build a variant by name, exactly like a struct literal — clearer when the positional order isn’t obvious, and free to reorder:
let c = Circle(radius: 2.0)
let r = Rect(width: 3.0, height: 4.0) // or Rect(height: 4.0, width: 3.0)
Both forms mean the same thing; pick whichever reads better.
match: handling every case
You take an enum apart with match, which checks the value against each variant and runs the
matching arm, binding that variant’s fields as local names:
fn area(s: Shape) -> float {
match s {
case Circle(r) { return 3.14159 * r * r }
case Rect(w, h) { return w * h }
case Origin { return 0.0 }
}
}
Three properties make match trustworthy:
- It’s exhaustive. Handle every variant or the program won’t compile. Add a fourth shape
six months from now and the compiler will march you to every
matchthat forgot about it. - There’s no fallthrough. The first matching arm wins and that’s the end of it; no
breakneeded, no accidental tumble into the next case. - It binds fields cleanly. In
case Circle(r), the nameris the circle’s radius, scoped to just that arm.
The catch-all: case _
When you genuinely want “everything else,” case _ handles every variant an earlier arm
didn’t. It keeps a match exhaustive without listing the whole world, and it must come last
(anything after it would be unreachable):
match colour {
case Red { return 1 }
case Green { return 2 }
case _ { return 0 } // any other colour
}
(Be aware that _ inside a variant pattern, like case Some(_), is just an ordinary
ignored binding — “there’s a value here but I don’t care about it” — not the catch-all.)
A couple of fine points
Within a module, variant names are distinct — two enums you can see at once can’t both have a
Red, and none may collide with the prelude’s Some/None/Ok/Err. Across different modules
they’re free to repeat, since each module sees only its own. Either way, you can name a variant bare
(Origin) or spelled out through its enum (Shape.Origin, Circle or Shape.Circle), whichever
reads better; they mean the same thing.
And one syntactic quirk you’ll meet eventually: in the header of an if, for, or match,
Ingle switches off struct-literal syntax so that the { can unambiguously start the block. If
you ever genuinely need a struct literal right there, wrap it in parentheses. In practice this
almost never comes up, but now it won’t puzzle you when it does.
Fireside trivia. Sum types — the formal name for these — are decades old (ML had them in the 1970s) and spent a long time confined to academic and functional languages while the mainstream made do with null and inheritance. Their slow conquest of industry is one of the quiet happy endings in language design: Rust, Swift, Kotlin, TypeScript, and modern C# all have them now. Ingle was never going to not have them. They were the first thing on the list.