Skip to content

Enums

Declare closed alternatives with unit, tuple, and struct variants, match their payloads, and make them generic or recursive.

Updated View as Markdown

An enum is a named, closed set of alternatives. A value is exactly one variant at a time, and match can prove that every alternative was considered.

enum Direction { North, East, South, West }

fn opposite(d: Direction): Direction {
    return match d {
        North => Direction.South
        East => Direction.West
        South => Direction.North
        West => Direction.East
    }
}

fn f(): bool {
    return opposite(Direction.North) == Direction.South
}

Enum and variant names use PascalCase. Variants are separated by commas or newlines — enum Direction { North, East } and the multi-line form above are the same declaration.

Unit variants

A unit variant carries no payload. Name it bare when the expected type is known, or qualify it with the enum name anywhere else.

enum Direction { North, East, South, West }

fn f(): int {
    annotated: Direction = North        // expected type resolves the bare name
    qualified := Direction.East         // no expectation, so qualify

    return match annotated {
        North => 0
        East => 1
        South => 2
        West => 3
    } + match qualified {
        North => 0
        _ => 10
    }
}

Qualification is a source-level disambiguator only: North and Direction.North build the same value. Reach for the qualified form when two enums in scope share a variant name.

enum Direction { North, East }
enum Turn { North, East }

fn f(): int {
    d := Direction.North
    t := Turn.East
    a := match d { North => 0, East => 1 }
    b := match t { North => 0, East => 1 }
    return a + b
}

Tuple variants

A tuple variant carries positional payloads. Construct with call syntax and destructure with the same shape.

enum Shape {
    Point
    Circle(float)
    Rectangle(float, float)
}

fn area(s: Shape): float {
    return match s {
        Point => 0.0
        Circle(radius) => 3.14159 * radius * radius
        Rectangle(width, height) => width * height
    }
}

fn total(shapes: []Shape): float {
    mut sum := 0.0
    for s in shapes {
        sum += area(s)
    }
    return sum
}

fn f(): float {
    return total([Circle(1.0), Rectangle(2.0, 3.0), Point])
}

Arity is part of the constructor: Rectangle(2.0) and Circle(1.0, 2.0) are both errors, and a pattern must bind exactly as many positions as the variant declares.

Struct variants

A struct variant names its payload fields, which is worth the extra typing as soon as two payloads have the same type.

enum Message {
    Quit
    Move { x: int, y: int }
    Write { text: string }
}

fn describe(m: Message): string {
    return match m {
        Move { x, y } => "move ${x},${y}"
        Write { text } => "write ${text}"
        Quit => "quit"
    }
}

fn f(): string {
    return describe(Move { x: 10, y: 20 })
}

One enum may freely mix the three variant shapes:

enum State {
    Idle
    Running { pid: int }
    Done(int)
}

fn exit_code(s: State): int {
    return match s {
        Idle => -1
        Running { pid } => pid
        Done(code) => code
    }
}

fn f(): int {
    started: State = Running { pid: 42 }
    return exit_code(started) + exit_code(State.Done(0))
}

Note the construction in that example. Done(0) — a tuple variant — can be written qualified as State.Done(0). A struct variant cannot: State.Running { pid: 42 } does not resolve, because State.Running is read as a value path rather than a literal head.

enum State { Idle, Running { pid: int } }

fn f(): int {
    s: State = State.Running { pid: 42 }
    return match s { Idle => 0, Running { pid } => pid }
}

Write the bare Running { pid: 42 } and let the expected type pick the enum, as in the working example above.

Exhaustiveness

Because the variant set is closed, a match that omits one is rejected.

enum Direction { North, East, South, West }

fn f(d: Direction): int {
    return match d {
        North => 0
        East => 1
    }
}

That reports ATOLL2010: non-exhaustive match on Direction — missing: South, West. The practical consequence is that adding a variant turns into a compile-time worklist of every place that must decide what to do about it.

A wildcard arm silences that signal deliberately. Use it for a genuine policy (“anything else is not horizontal”), not to save three lines:

enum Direction { North, East, South, West }

fn is_vertical(d: Direction): bool {
    return match d {
        North => true
        South => true
        _ => false
    }
}

fn f(): bool { return is_vertical(Direction.East) }

Guards narrow an already-matched variant but do not cover it. Every guarded arm still needs an unguarded fallback for the same variant:

enum Status { Code(int), Missing }

fn classify(s: Status): string {
    return match s {
        Code(v) if v >= 500 => "server"
        Code(v) if v >= 400 => "client"
        Code(_) => "ok"
        Missing => "missing"
    }
}

fn f(): string { return classify(Code(503)) }

Alternation collapses several variants into one arm:

enum Direction { North, East, South, West }

fn is_horizontal(d: Direction): bool {
    return match d {
        East | West => true
        North | South => false
    }
}

fn f(): bool { return is_horizontal(Direction.West) }

Integer discriminants

Variants may be given explicit integer discriminants, which is how you pin an enum to an external protocol’s numbering.

enum HttpStatus {
    Ok = 200
    NotFound = 404
    Teapot = 418
}

fn f(): bool {
    return HttpStatus.Teapot == HttpStatus.NotFound
}

An integer-valued enum is still an enum, not an int. There is no implicit numeric conversion — HttpStatus.Ok as int is rejected — so if you need the number, write the mapping you want and keep it next to the declaration:

enum HttpStatus { Ok = 200, NotFound = 404, Teapot = 418 }

fn HttpStatus.code(self): int {
    return match self {
        Ok => 200
        NotFound => 404
        Teapot => 418
    }
}

fn f(): int { return HttpStatus.NotFound.code() }

Do not mix explicit discriminants with data-carrying variants in one enum, and do not treat an implicit discriminant as a stable wire value.

Methods

An enum can declare methods directly in its body, in an impl block, or with a receiver-prefix function — the same three spellings a struct has.

enum Direction {
    North
    East
    South
    West

    fn label(self): string {
        return match self {
            North => "north"
            East => "east"
            South => "south"
            West => "west"
        }
    }
}

fn f(): string { return Direction.South.label() }
enum Priority { Low, Normal, High }

impl Priority {
    fn weight(self): int {
        return match self {
            Low => 1
            Normal => 5
            High => 10
        }
    }
}

fn f(): int { return Priority.High.weight() }

Trait implementations work the same way. Implementing Display is what makes an enum usable inside a "${...}" template — without it the template is an error:

enum Level { Debug, Info, Warn }

impl Display for Level {
    fn to_string(self): string {
        return match self {
            Debug => "debug"
            Info => "info"
            Warn => "warn"
        }
    }
}

fn line(l: Level, message: string): string {
    return "[${l}] ${message}"
}

fn f(): string { return line(Level.Warn, "disk almost full") }

@derive covers the mechanical implementations. A derived Hashable plus Equatable is what lets an enum be a Map key:

@derive(Equatable, Hashable, Debug, Clone)
enum Color { Red, Green, Blue }

fn counts(picks: []Color): int {
    mut tally: Map[Color, int] = Map.new()
    for c in picks {
        tally.put(c, (tally.get(c) ?? 0) + 1)
    }
    return tally.get(Color.Red) ?? 0
}

fn f(): int { return counts([Color.Red, Color.Blue, Color.Red]) }

Generics

Type parameters go in square brackets and are substituted before payload checking.

enum Either[L, R] {
    Left(L)
    Right(R)
}

fn render(v: Either[int, string]): string {
    return match v {
        Left(n) => "number ${n}"
        Right(s) => s
    }
}

fn f(): string {
    chosen: Either[int, string] = Right("hello")
    return render(chosen)
}

Inference takes the type argument from an annotation, a parameter type, or any other expected type; supply an annotation when nothing else constrains it.

Generic enums can carry methods too:

enum Slot[T] { Empty, Filled(T) }

impl Slot[T] {
    fn or_default(self, fallback: T): T {
        return match self {
            Empty => fallback
            Filled(v) => v
        }
    }
}

fn f(): int {
    s: Slot[int] = Filled(3)
    e: Slot[int] = Empty
    return s.or_default(0) + e.or_default(7)
}

Recursion

A variant that stores the enum itself by value has no finite size and is rejected:

enum Node {
    Leaf(int)
    Pair(Node, Node)
}

That reports ATOLL3011: Node contains itself by value, so it has no finite size. Route the recursion through a managed container — a list is the usual choice — and the layout becomes finite:

enum Expression {
    Literal(int)
    Sum([]Expression)
    Product([]Expression)
}

fn eval(e: Expression): int {
    return match e {
        Literal(v) => v
        Sum(parts) => {
            mut total := 0
            for p in parts { total += eval(p) }
            total
        }
        Product(parts) => {
            mut total := 1
            for p in parts { total *= eval(p) }
            total
        }
    }
}

fn f(): int {
    // (1 + 2) * 5
    return eval(Product([Sum([Literal(1), Literal(2)]), Literal(5)]))
}

The same rule applies to a generic recursive enum, and to cycles that pass through a second named type. Moving a direct cycle through another struct does not make it finite; only an indirection does.

enum Tree[T] {
    Leaf(T)
    Branch([]Tree[T])
}

fn size[T](t: Tree[T]): int {
    return match t {
        Leaf(_) => 1
        Branch(kids) => {
            mut n := 0
            for k in kids { n += size(k) }
            n
        }
    }
}

fn f(): int {
    t: Tree[string] = Branch([Leaf("a"), Leaf("b")])
    return size(t)
}

Identity

Enum identity is nominal. Two declarations with identical variants are still two types, and their constructors do not interchange.

enum InputState { Ready, Closed }
enum OutputState { Ready, Closed }

fn take(s: InputState): int { return 0 }

fn f(): int {
    out: OutputState = OutputState.Ready
    return take(out)
}

That is the point: an unrelated domain does not become assignable just because its current variant list happens to agree.

Evolution

For an enum that other modules consume, treat the declaration as a contract:

  • adding a variant breaks every exhaustive match that lacks a wildcard;
  • renaming or removing a variant breaks construction and patterns;
  • changing a payload shape breaks both;
  • changing explicit discriminants breaks persisted or foreign data.

The exhaustiveness break is a feature. If you would rather not have it, add the wildcard deliberately and document what “everything else” means.

A composed example

A tiny command parser: one enum with all three variant shapes, an error enum for the failure, and a catch that converts the failure into a message.

enum Command {
    Ping
    Echo(string)
    Assign { key: string, value: string }
}

error CommandError {
    Unknown { name: string }
    MissingArgument { verb: string }
}

fn parse(line: string): Command ! CommandError {
    parts := line.split(" ")
    head := parts.get(0) ?? ""

    if head == "ping" { return Ping }

    if head == "echo" {
        if parts.len() < 2 { error MissingArgument { verb: head } }
        return Echo(parts.get(1) ?? "")
    }

    if head == "assign" {
        if parts.len() < 3 { error MissingArgument { verb: head } }
        return Assign { key: parts.get(1) ?? "", value: parts.get(2) ?? "" }
    }

    error Unknown { name: head }
}

fn run(line: string): string {
    cmd := parse(line) catch {
        Unknown { name } => return "unknown command: ${name}"
        MissingArgument { verb } => return "${verb} needs an argument"
    }
    return match cmd {
        Ping => "pong"
        Echo(text) => text
        Assign { key, value } => "${key}=${value}"
    }
}

fn f(): string { return run("assign color red") }

Every arm of the final match is required, so adding a Quit variant to Command makes run fail to compile until it is handled — which is exactly the migration prompt you want.

Use a named enum when alternatives deserve stable names, payloads, methods, or derives. For a one-off combination of types that already carry their own meaning, an anonymous union is lighter.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close