Chapter 8
Tag unions: billboard critters
Four kinds of big animal stand at the bends in the ride's road: elephants, giraffes, zebras and rhinos. Each animal stands with its baby:
Here we learn tag unions and match: a type that is exactly one of a few tags, and code that handles each tag in turn. A nominal type and a tag that carries a value come along with them.
A tag union lists every case §
Each emoji shows its animal from the side, so a billboard faces left or right. The Billboard module gives those two ways a type of their own:
## Which way a billboard faces on screen.
Facing : [FacesLeft, FacesRight]
The type is a list of tags in square brackets. A tag is a value in its own right: FacesLeft is a value, and so is FacesRight. A type made of tags is a tag union, and a value of a tag union is always exactly one of its tags. So the type lists every case there is.
A match handles every case §
The “Things” chapter showed the annotation of Billboard.scenery_sprite alone. Here's the whole function:
## How a billboard looks. An emoji faces left, so a billboard facing right
## is its still mirrored.
scenery_sprite : Billboard, Sprite.Setting -> Sprite
scenery_sprite = |Billboard.(billboard), setting| {
flip =
match billboard.facing {
FacesLeft => AsDrawn
FacesRight => Mirrored
}
Sprite.from_shapes(
StillShapes.standing_still_shapes(
setting,
{ height: billboard.height, still: billboard.emoji.still(), flip },
),
)
}
A Billboard is opaque, and Billboard.(billboard) in the arguments opens it into a record named billboard, so the function can read billboard.facing and the other fields. Only the Billboard module can write Billboard.(billboard).
Braces hold a block or a record: a block holds lines of code, and a record holds fields with colons. The block names flip, and its last expression makes the sprite. billboard.emoji.still() returns the emoji's traced still: a list of filled polygons, measured in units of the animal's own height, so one still serves a cow far off and a cow by the road. StillShapes.standing_still_shapes scales it to the billboard's height, and Sprite.from_shapes makes the shapes a sprite. In that last record, flip alone is short for flip: flip.
Every emoji faces left, so a billboard that faces right needs its emoji mirrored. The match makes that choice: each line under it takes one tag of billboard.facing and returns a tag. FacesLeft gives AsDrawn, and FacesRight gives Mirrored. Those two are the tags of another tag union, StillShapes.Flip, and flip names the one the match returns. The compiler infers the type of flip from where it's used.
A match handles every tag of its type, and the compiler checks that it does. Leave out the FacesRight line, and the compiler stops with an error that names it:
This match expression doesn't cover all possible cases.
Missing patterns:
FacesRight
Here, getting the types right pays the most. A tag union lists every case, so when a new tag joins it, the compiler names every match that has yet to handle it.
A nominal type sets its tags apart §
The bend animals are a tag union too, with four tags:
# ...
BendAnimal := [Elephant, Giraffe, Zebra, Rhino].{
# ...
Each tag is an animal. BendAnimal uses :=, so it's a nominal type: a new type, apart from any other tag union with the same tags. The species_of function turns each one into its species record, an emoji and a grown height, like the cow's in the “Things” chapter:
## The species of a bend's animal.
species_of : BendAnimal -> Billboard.Species
species_of = |animal|
match animal {
Elephant => { emoji: EmojiStill.elephant, adult_height: Distance.metres(2.8) }
Giraffe => { emoji: EmojiStill.giraffe, adult_height: Distance.metres(4.5) }
Zebra => { emoji: EmojiStill.zebra, adult_height: Distance.metres(1.6) }
Rhino => { emoji: EmojiStill.rhino, adult_height: Distance.metres(2.2) }
}
The match has one line for each of the four tags, so every bend animal has a species, and a grown giraffe is always 4.5 metres tall.
A tag carries a value §
A bend is quiet, or it holds one thing: an animal and its baby, or a pond of ducks. The Segment module's BendLife type says what's at a bend:
## What lives at the bend that ends a segment: nothing, a safari animal and
## its baby, or a pond with its ducks.
BendLife := [Quiet, Animal(BendAnimal), DuckPond]
Quiet and DuckPond are plain tags, like the ones above. Animal(BendAnimal) is a tag that carries a value: an Animal always comes with one BendAnimal. So Animal(Giraffe) is a whole value, a bend with a giraffe at it. The route's plan stores a BendLife for each bend, and a BendLife is exactly one of its three tags, so each bend gets one thing.
The function that returns a bend's billboards takes the bend's life and the bend itself, a Geom.Bend, and takes the life apart with a match:
## The billboards of `bend_life`, placed on the segment that ends at `bend`:
## the animal and its baby, or the ducks on the pond, whose places are measured
## from the bend.
bend_billboards : Segment.BendLife, Geom.Bend -> List(Geom.Placed(Billboard))
bend_billboards = |bend_life, bend|
match bend_life {
Animal(animal) => animal.bend_billboards(bend)
DuckPond => Pond.placed_ducks(bend)
Quiet => []
}
The Animal(animal) line matches a bend with any animal at it, and names the animal it carries animal, ready to use. Then animal.bend_billboards(bend) calls a different function with the same name. animal is a BendAnimal, so the call goes to the bend_billboards that comes with BendAnimal, which places the animal and its baby. A duck pond gets its ducks from the Pond module, and a quiet bend gets the empty list, []. So a tag's value rides along with it, and the match hands it to the code that needs it.
Elephants, mostly §
The route's plans say what lives at each bend, and most of them say Animal(Elephant). Only a handful say Animal(Giraffe), Animal(Zebra), Animal(Rhino) or DuckPond. We chose that mix: the elephants are the fixture, so a rare animal is a surprise.
A tag union lists every case, a match handles each one, a nominal type sets its tags apart, and a tag can carry a value. The emojis came ready-made, traced ahead of time. Next, we build shapes of our own: a triangle and a rectangle.