Heroes / Guide / chapter 4

Variants and match

Defining the cases of a value and handling them with match.

A record contains all its fields: a point has an x and a y. A variant represents exactly one declared case. For example, a token, an element of the text the compiler reads, can be a number, a word or a plus sign. Some cases contain additional data.

Declaring one

variant Token
    number
        value: i64
    word
        text: str
    plus
    times
    open
    close
examples/gallery/02-tokens.hero, lines 7–15

The type has six cases. number and word contain data declared as fields indented below the case name. The other four have no fields: plus is already a complete value.

Reading one: match

match identifies a variant’s case and lets you access its fields in the same branch:

# What a token is called in a message.
function name_of(t: Token) -> str
    return match t
        .number _ => "a number"
        .word _   => "a word"
        .plus     => "`+`"
        .times    => "`*`"
        .open     => "`(`"
        .close    => "`)`"
examples/gallery/02-tokens.hero, lines 17–25

Each branch names the case with a leading dot, followed by => and the result. If the case contains data, you can give it a name: in .number n, n.value is the number inside. Here the data is unused, so the branch uses _, which declares no name and can be repeated.

match is an expression: it has a value, which is why this whole function is one return. It can also stand alone as a statement when the arms do something rather than produce something.

Every case, or it does not compile

Leave one out and the build stops. This is the file above with its last arm deleted:

$ heroes check tokens.hero
error[non_exhaustive]: this `match` does not cover `.close`
  at tokens.hero:19:12
     |
  19 |     return match t
     |            ^

A _ branch is forbidden for variants

For a variant, Heroes forbids a _ branch that would handle all unlisted cases. The message explains why:

$ heroes check tokens.hero
error[wildcard_on_variant]: `_` is not allowed as an arm over `Token` — name every case, so that adding one breaks this `match` instead of silently falling through
  at tokens.hero:25:9
     |
  25 |         _         => "something else"
     |         ^

You can use _ in a match on an i64 or a str to handle all unlisted values. For these types, writing a branch for every value would be impractical.

Adding a case requires updating each match

Add a seventh case, minus, to Token and check the program again:

$ heroes check tokens.hero
error[non_exhaustive]: this `match` does not cover `.minus`
  at tokens.hero:20:12
     |
  20 |     return match t
     |            ^

error[non_exhaustive]: this `match` does not cover `.minus`
  at tokens.hero:30:12
     |
  30 |     return match t
     |            ^

error[non_exhaustive]: this `match` does not cover `.minus`
  at tokens.hero:38:12
     |
  38 |     return match t
     |            ^

The compiler reports three errors, one for each match that does not handle the new case. You must decide what to call the minus sign, its precedence and whether it is an operator. Compilation can continue once all cases are handled.

Two cases, one answer

When several cases produce the same result, | lets you list them in one branch:

function is_operator(t: Token) -> bool
    return match t
        .plus | .times                       => true
        .number _ | .word _ | .open | .close => false
examples/gallery/02-tokens.hero, lines 36–39

Every case remains explicit. Adding minus therefore requires updating this function too, deciding whether it should return true or false for the new case.

Making one

function main()
    # The annotation is required and it is not ceremony: a case says which
    # case it is, never which variant, so a literal list of them has nothing
    # to infer from. Name the type once and the whole list is checked.
    line: [Token] = [.open, .number(value: 12), .plus, .word(text: "x"), .close]

    for t in line
        print(name_of(t))
examples/gallery/02-tokens.hero, lines 46–53
$ heroes run examples/gallery/02-tokens.hero
`(`
a number
`+`
a word
`)`

Write a case without fields as .plus. For a case with fields, use .number(value: 12), naming the fields as for a record. The leading dot identifies the case, but the compiler also needs its type. The annotation : [Token] supplies it for every element in the list.

Next: Failure is a value, which is this chapter’s idea applied to the one case every program has: it worked, or it did not. Back to the chapter list. ← Previous: Records and functions

Run a program of your own: it takes four commands. Or take the whole language in one file, and every program already written in it.