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
matchthat 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.