The Tessera Book
Chapter 6

Enums and Pattern Matching

A struct holds several values at once. An enum holds one of several alternatives, each of which can carry its own data. Together with match, enums are how Tessera programs describe choices, and the compiler makes sure every choice is handled.

Defining an enum

enum Shape {
    Circle(Float),
    Rect(Float, Float),
    Point,
}

fn area(self: Shape) -> Float {
    match self {
        Circle(r) => 3.14159 * r * r,
        Rect(w, h) => w * h,
        Point => 0.0,
    }
}

fn main(root: Root) {
    let shapes = [Circle(1.0), Rect(2.0, 3.5), Point]
    for s in shapes {
        root.stdout.print("{s}: {s.area()}")
    }
}
Listing 6-1: An enum of shapes, and a method that matches on it examples/ch06-01-shapes/main.tsr
$ tessera run main.tsr
Circle(1.0): 3.14159
Rect(2.0, 3.5): 7.0
Point: 0.0

Shape has three variants. Circle carries one Float, Rect two, and Point nothing. A value of type Shape is exactly one of them. You build one by naming the variant: Circle(1.0), Point.

You've already been using two enums that come with the language:

enum Option[T] {
    Some(T),
    None,
}

enum Result[T, E] {
    Ok(T),
    Err(E),
}

Option and Result are not special syntax, just enums with type parameters. Everything in this chapter applies to them.

match

match compares a value with a list of patterns and runs the first arm that fits. A pattern like Rect(w, h) matches a Rect and binds its two values to w and h for that arm. match is an expression: each arm produces a value, and all arms must produce the same type.

Names in patterns follow a simple rule: a name starting with an uppercase letter is a variant, and a lowercase one binds whatever is there. _ matches anything and binds nothing.

Matches are exhaustive

A match must cover every possible value. Forget a case and the program doesn't compile:

This program does not compile
enum Shape {
    Circle(Float),
    Rect(Float, Float),
    Point,
}

fn corners(s: Shape) -> Int {
    match s {
        Rect(_, _) => 4,
        Point => 0,
    }
}

fn main(root: Root) {
    root.stdout.print("{corners(Point)}")
}
Listing 6-2: A match that forgets Circle examples/ch06-02-missing-arm/main.tsr
$ tessera check main.tsr
error[E0204]: `match` does not cover `Circle(_)`
  --> main.tsr:8:5
   |
 8 |     match s {
   |     ^^^^^^^^^
  = help: add the missing arms

1 error

This is one of the most useful checks the compiler makes. When you add a variant to an enum, every match that doesn't handle it becomes an error, so the compiler gives you the list of places to update. The error also comes with a fix that adds the missing arms, which your editor can apply.

When matching on Int or Str, which have too many values to list, you need a _ or a name as the last arm.

Guards

Sometimes a pattern alone can't say what you mean. An arm can add a condition, a guard, with if after the pattern:

struct Order {
    total: Int,
    paid: Bool,
}

fn describe(order: Option[Order]) -> Str {
    match order {
        Some(o) if o.total > 1000 => "large",
        Some(o) if o.paid => "paid",
        Some(_) => "open",
        None => "none",
    }
}

fn main(root: Root) {
    let orders = [
        Some(Order { total: 5000, paid: false }),
        Some(Order { total: 20, paid: true }),
        Some(Order { total: 20, paid: false }),
        None,
    ]
    for o in orders {
        root.stdout.print(describe(o))
    }
}
Listing 6-3: Arms with guards examples/ch06-03-guards/main.tsr
$ tessera run main.tsr
large
paid
open
none

An arm with a guard applies only when the pattern matches and the guard is true; otherwise matching goes on to the next arm. Because a guard might be false, a guarded arm doesn't count when the compiler checks that the match is exhaustive. That's why Some(_) is still needed after the two guarded Some arms.

Tuples and patterns

You met tuples briefly in chapter 4. A tuple groups a few values without naming them: (1, "one") is a (Int, Str). Tuples are handy for returning two things from a function, and for matching on two things at once:

fn fizzbuzz(n: Int) -> Str {
    match (n % 3, n % 5) {
        (0, 0) => "FizzBuzz",
        (0, _) => "Fizz",
        (_, 0) => "Buzz",
        _ => "{n}",
    }
}

fn divmod(a: Int, b: Int) -> (Int, Int) {
    (a / b, a % b)
}

fn main(root: Root) {
    root.stdout.print((1..<16).map(fizzbuzz).join(" "))

    let (q, r) = divmod(17, 5)
    root.stdout.print("17 = 5 * {q} + {r}")

    for (name, age) in [("ana", 31), ("bo", 25)] {
        root.stdout.print("{name} is {age}")
    }
}
Listing 6-4: Matching on a tuple; unpacking tuples examples/ch06-04-tuples/main.tsr
$ tessera run main.tsr
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
17 = 5 * 3 + 2
ana is 31
bo is 25

match (n % 3, n % 5) builds a tuple and matches its parts together; FizzBuzz becomes a table. The compiler checks tuples column by column, so it knows these four arms cover every case.

let (q, r) = divmod(17, 5) unpacks a tuple into names, and for (name, age) in ... does the same for each item of a list. You can also read a tuple's parts by position, as t.0 and t.1.

(1..<16).map(fizzbuzz) passes the function fizzbuzz itself to map; functions are values, as you'll see in chapter 9.

Recursive enums

A variant can hold a value of its own enum type. That's how you describe trees, like this one for arithmetic expressions:

// An arithmetic expression as a tree. The enum holds itself through its
// variants, which is how recursive types are built.
enum Expr {
    Num(Int),
    Add(Expr, Expr),
    Mul(Expr, Expr),
    Neg(Expr),
}

fn eval(e: Expr) -> Int {
    match e {
        Num(n) => n,
        Add(a, b) => eval(a) + eval(b),
        Mul(a, b) => eval(a) * eval(b),
        Neg(a) => -eval(a),
    }
}

fn show(e: Expr) -> Str {
    match e {
        Num(n) if n < 0 => "({n})",
        Num(n) => "{n}",
        Add(a, b) => "({show(a)} + {show(b)})",
        Mul(a, b) => "{show(a)} * {show(b)}",
        Neg(a) => "-{show(a)}",
    }
}

fn main(root: Root) {
    let e = Mul(Add(Num(2), Num(3)), Neg(Num(4)))
    root.stdout.print("{show(e)} = {eval(e)}")
}
Listing 6-5: An expression tree, evaluated and printed by matching examples/ch06-05-expr/main.tsr
$ tessera run main.tsr
(2 + 3) * -4 = -20

Patterns nest, so Add(a, b) binds both sides of an addition, and each side is itself an Expr. eval and show follow the shape of the type: one arm per variant, with a recursive call for each part. When you add a variant, say Sub, the compiler points at both functions.

Enums, List and Map can hold their own type, because they store their contents indirectly. A struct that contains itself directly would be infinitely large, and the compiler rejects it; put the recursive part in an Option, a List or an enum.

More patterns

The patterns so far match one shape per arm. A few more forms let a single arm say more: alternatives, a way to skip the parts you don't care about, patterns for lists and structs, and a way to name a whole value while taking it apart.

All of them work in match. let and for unpack only names, _ and tuples, as in let (q, r) = ..., because a pattern there has to fit every value; anything that might not fit needs a match, which says what happens when it doesn't.

Or-patterns

When several cases are handled the same way, list them in one arm with | between them:

enum Day {
    Mon,
    Tue,
    Wed,
    Thu,
    Fri,
    Sat,
    Sun,
}

enum Shape {
    Circle(Float),
    Ring(Float, Float),
    Rect(Float, Float),
}

fn kind(d: Day) -> Str {
    match d {
        Sat | Sun => "weekend",
        _ => "weekday",
    }
}

fn width(s: Shape) -> Float {
    match s {
        Circle(r) | Ring(r, _) => 2.0 * r,
        Rect(w, _) => w,
    }
}

fn plan(next: Option[Day]) -> Str {
    match next {
        Some(Sat | Sun) => "rest",
        Some(_) => "work",
        None => "nothing planned",
    }
}

fn main(root: Root) {
    for d in [Fri, Sat] {
        root.stdout.print("{d} is a {kind(d)}")
    }
    for s in [Circle(1.0), Ring(3.0, 2.0), Rect(4.0, 1.5)] {
        root.stdout.print("{s} is {width(s)} wide")
    }
    root.stdout.print(plan(Some(Sun)))
}
Listing 6-6: Or-patterns: several shapes in one arm examples/ch06-06-or-patterns/main.tsr
$ tessera run main.tsr
Fri is a weekday
Sat is a weekend
Circle(1.0) is 2.0 wide
Ring(3.0, 2.0) is 6.0 wide
Rect(4.0, 1.5) is 4.0 wide
rest

Sat | Sun => "weekend" matches either day. The alternatives are tried from left to right, and the arm applies as soon as one of them fits. An or-pattern can also sit inside another pattern: Some(Sat | Sun) matches a Some holding either day, without repeating the Some.

Alternatives can bind names. Circle(r) | Ring(r, _) binds r to the radius of a circle, or to the outer radius of a ring, and the arm's body uses r the same way whichever one matched. For that to work, every alternative must bind the same names, with the same types. Bind a name in only one of them and the body couldn't know whether it exists:

This program does not compile
enum Shape {
    Circle(Float),
    Ring(Float, Float),
    Rect(Float, Float),
}

fn width(s: Shape) -> Float {
    match s {
        Circle(r) | Ring(r, inner) => 2.0 * r,
        Rect(w, _) => w,
    }
}

fn main(root: Root) {
    root.stdout.print("{width(Circle(1.0))}")
}
Listing 6-7: An alternative that binds a name the other doesn't examples/ch06-07-or-mismatch/main.tsr
$ tessera check main.tsr
error[E0210]: `inner` is bound in this alternative but not in the first one
  --> main.tsr:9:21
   |
 9 |         Circle(r) | Ring(r, inner) => 2.0 * r,
   |                     ^^^^^^^^^^^^^^
  = note: every alternative of `A | B` must bind the same names, with the same types

1 error

The fix is to write _ for the inner radius, as listing 6-6 does, or to give the two shapes separate arms.

The rest: ..

Variants and tuples with many items make for long patterns, mostly underscores. .. stands for all the items you don't write out:

enum Entry {
    Request(Str, Str, Int, Int),
    Note(Str),
}

fn summary(e: Entry) -> Str {
    match e {
        Request(_, path, 500 | 503, ..) => "{path} failed",
        Request(method, path, ..) => "{method} {path}",
        Note(text) => text,
    }
}

fn slowest(times: List[(Str, Int, Int)]) -> Str {
    match times.sort_by(|t| t.2).last() {
        Some((path, .., ms)) => "{path} took {ms} ms",
        None => "no requests",
    }
}

fn main(root: Root) {
    let log = [
        Request("GET", "/", 200, 12),
        Request("POST", "/upload", 503, 950),
        Note("deploy finished"),
    ]
    for e in log {
        root.stdout.print(summary(e))
    }
    root.stdout.print(slowest([("/", 200, 12), ("/upload", 503, 950), ("/about", 200, 40)]))
}
Listing 6-8: .. skips the items an arm doesn't need examples/ch06-08-rest/main.tsr
$ tessera run main.tsr
GET /
/upload failed
deploy finished
/upload took 950 ms

Request(method, path, ..) matches any Request and takes its first two values; the status and the time are skipped. .. can go anywhere among the items, so (path, .., ms) takes the first and the last item of a tuple, and Request(_, path, 500 | 503, ..) mixes it with an or-pattern on the status. Each pattern can use .. once, since with two it would be unclear which items each one covers. The compiler knows how many items the type has, so .. can also stand for none.

List patterns

A list pattern matches a list by its items, written in square brackets:

fn describe(xs: List[Int]) -> Str {
    match xs {
        [] => "nothing",
        [only] => "just {only}",
        [first, .., last] => "{first} up to {last}",
    }
}

fn run(line: Str) -> Str {
    match line.split(" ").filter(|w| w != "") {
        [] => "say something",
        ["add", name] => "added {name}",
        ["remove", ..names] => "removed {names.len()}: {names.join(", ")}",
        [.., "please"] => "no need to say please",
        [command, ..] => "unknown command: {command}",
    }
}

fn total(xs: List[Int]) -> Int {
    match xs {
        [] => 0,
        [first, ..rest] => first + total(rest),
    }
}

fn main(root: Root) {
    for xs in [[], [7], [1, 2, 3]] {
        root.stdout.print(describe(xs))
    }
    for line in ["", "add milk", "remove eggs flour", "dance please", "dance"] {
        root.stdout.print(run(line))
    }
    root.stdout.print("{total([1, 2, 3, 4])}")
}
Listing 6-9: Matching lists by their length and items examples/ch06-09-lists/main.tsr
$ tessera run main.tsr
nothing
just 7
1 up to 3
say something
added milk
removed 2: eggs, flour
no need to say please
unknown command: dance
10
  • [] matches the empty list, [only] a list of exactly one item, and ["add", name] a list of exactly two whose first item is "add".
  • With .., a pattern matches any list at least as long as the items it writes: [command, ..] needs at least one item, and [.., "please"] looks at the end instead. [first, .., last] needs at least two, so first and last are always different items.
  • In a list, the rest can have a name. ["remove", ..names] binds names to a new list of the items after "remove", and [..init, last] would bind the items before the last one.

run reads like the description of a tiny command language: split the line into words, then match on the words. total shows the classic recursive form: a list is either empty, or a first item followed by the rest. Keep in mind that rest is a new list each time, so for long lists fold or a loop does the same work without the copies.

Exhaustiveness works for lists too. The compiler counts lengths: [] covers length 0, [only] length 1, and [first, .., last] every length from 2 up, so describe covers every list. Leave out the middle case, and the compiler names the length that's missing:

This program does not compile
fn describe(xs: List[Int]) -> Str {
    match xs {
        [] => "nothing",
        [first, .., last] => "{first} up to {last}",
    }
}

fn main(root: Root) {
    root.stdout.print(describe([1, 2, 3]))
}
Listing 6-10: A list match with a gap examples/ch06-10-list-missing/main.tsr
$ tessera check main.tsr
error[E0204]: `match` does not cover `[_]`
  --> main.tsr:2:5
   |
 2 |     match xs {
   |     ^^^^^^^^^^
  = help: add the missing arms

1 error

[_] is a list of one item, the one length that neither [] nor [first, .., last] matches.

Struct patterns

A struct can be matched by its fields, written like a struct literal with patterns in place of values:

struct Point {
    x: Int,
    y: Int,
}

struct Line {
    from: Point,
    to: Point,
}

fn where_is(p: Point) -> Str {
    match p {
        Point { x: 0, y: 0 } => "at the origin",
        Point { x, y: 0 } => "on the x axis, at {x}",
        Point { x: 0, .. } => "on the y axis",
        Point { x, y } if x == y => "on the diagonal",
        _ => "somewhere else",
    }
}

fn kind(l: Line) -> Str {
    match l {
        Line { from: Point { x: x1, .. }, to: Point { x: x2, .. } } if x1 == x2 => "vertical",
        Line { from: Point { y: y1, .. }, to: Point { y: y2, .. } } if y1 == y2 => "horizontal",
        _ => "slanted",
    }
}

fn main(root: Root) {
    let points = [
        Point { x: 0, y: 0 },
        Point { x: 4, y: 0 },
        Point { x: 0, y: -2 },
        Point { x: 3, y: 3 },
        Point { x: 1, y: 5 },
    ]
    for p in points {
        root.stdout.print("({p.x}, {p.y}) is {where_is(p)}")
    }
    let up = Line { from: Point { x: 2, y: 0 }, to: Point { x: 2, y: 9 } }
    let across = Line { from: Point { x: 0, y: 1 }, to: Point { x: 5, y: 1 } }
    root.stdout.print("{kind(up)}, {kind(across)}")
}
Listing 6-11: Matching structs by their fields examples/ch06-12-struct-patterns/main.tsr
$ tessera run main.tsr
(0, 0) is at the origin
(4, 0) is on the x axis, at 4
(0, -2) is on the y axis
(3, 3) is on the diagonal
(1, 5) is somewhere else
vertical, horizontal

Each field is name: pattern. Point { x: 0, y: 0 } matches only the origin, and Point { x, y: 0 } matches any point on the x axis. A field written alone, like x, is short for x: x: it binds the field to a variable of the same name, which is what you want most of the time.

.. at the end stands for the fields you don't mention, so Point { x: 0, .. } matches any point whose x is 0, whatever its y. Without .., every field has to be named:

This program does not compile
struct Point {
    x: Int,
    y: Int,
}

fn on_y_axis(p: Point) -> Bool {
    match p {
        Point { x: 0 } => true,
        _ => false,
    }
}

fn main(root: Root) {
    root.stdout.print("{on_y_axis(Point { x: 0, y: 3 })}")
}
Listing 6-12: A struct pattern that leaves out a field examples/ch06-14-struct-missing/main.tsr
$ tessera check main.tsr
error[E0206]: missing field `y` in the pattern for `Point`
  --> main.tsr:8:9
   |
 8 |         Point { x: 0 } => true,
   |         ^^^^^^^^^^^^^^
  = note: name every field, or end the pattern with `..` to match any value of the others

1 error

Why not let a missing field mean "anything"? Because when someone adds a field to Point later, a pattern that names every field is one the compiler points at, so you get to decide whether the new field matters there. .. says, on purpose, that the other fields don't.

Struct patterns nest like every other pattern. In kind, Line { from: Point { x: x1, .. }, to: Point { x: x2, .. } } reaches into both ends of a line and names the two x values differently, so a guard can compare them. A struct from another module is written as its type is, geo.Point { x, .. }.

A struct has only one shape, so for exhaustiveness the compiler treats its fields like a tuple's items: Point { x: 0, .. } and Point { x: _, .. } together cover every point.

Naming the whole: as

Sometimes an arm needs a part of a value and the value itself. pattern as name matches what the pattern matches, and also binds the whole matched value to name:

struct Order {
    id: Int,
    items: List[Str],
    paid: Bool,
}

fn next_step(o: Option[Order]) -> Str {
    match o {
        None => "nothing to do",
        Some(Order { paid: false, .. } as order) => "ask for payment on order {order.id}",
        Some(Order { items: [], .. }) => "close the empty order",
        Some(Order { items: [first, ..] as all, .. }) => "pack {all.len()} items, starting with {first}",
    }
}

fn main(root: Root) {
    let orders = [
        None,
        Some(Order { id: 7, items: ["tea"], paid: false }),
        Some(Order { id: 8, items: [], paid: true }),
        Some(Order { id: 9, items: ["cups", "saucers", "spoons"], paid: true }),
    ]
    for o in orders {
        root.stdout.print(next_step(o))
    }
}
Listing 6-13: Binding a whole value with as examples/ch06-13-as-patterns/main.tsr
$ tessera run main.tsr
nothing to do
ask for payment on order 7
close the empty order
pack 3 items, starting with cups

Some(Order { paid: false, .. } as order) checks that the order isn't paid, and gives the arm the whole order to work with. [first, ..] as all binds the first item and the whole list. Without as, you would have to match on a name and then test the fields again with a guard, or rebuild the value from its parts.

as applies to everything before it, so A | B as x binds x whichever alternative matched. To name just one alternative, put it in parentheses, (A as x) | B; then, as with any or-pattern, every alternative has to bind x.

Arms that never match

Arms are tried in order, so an arm placed after one that already matches everything it would match can never run. That is almost always a mistake, and the compiler warns about it:

fn status(code: Int) -> Str {
    match code {
        200 | 204 => "ok",
        301 | 302 | 301 => "moved",
        _ => "something else",
        404 => "not found",
    }
}

fn main(root: Root) {
    root.stdout.print(status(404))
}
Listing 6-14: Two things that can never match examples/ch06-11-unreachable/main.tsr
$ tessera run main.tsr
warning[W0002]: this alternative can never match: the arms and alternatives before it cover every value it matches
  --> main.tsr:4:21
   |
 4 |         301 | 302 | 301 => "moved",
   |                     ^^^

warning[W0002]: this arm can never match: the arms before it cover every value it matches
  --> main.tsr:6:9
   |
 6 |         404 => "not found",
   |         ^^^

2 warnings
something else

The program still runs: a warning doesn't stop it, it points at something to look at. And here the warning is right. The _ arm takes every code that isn't 200, 204, 301 or 302, so 404 never reaches its arm and the program prints "something else". The same check finds an alternative that can't match, like the second 301, which here is harmless but often hides a typo for a code that was meant to be there. Moving 404 above _ and fixing the alternative makes both warnings go away.

Summary

Enums describe values that are one of several alternatives, each with its own data. match takes them apart, binds what's inside, and must cover every case, so adding a variant shows you every place that needs to handle it. Guards refine arms with a condition, tuples let you match on several values at once, and enums can be recursive. Or-patterns, .., list and struct patterns, and as make arms shorter and more precise, and the compiler warns about arms that can never match.

Our programs are getting bigger. Next, we'll split them into modules and use other people's code.