An enum is a closed set of named variants. It's the type
you reach for when a value is one of a fixed handful of things: a
direction, an input event, an AI state. Because the set is closed, the
compiler can check that a match handles every
case.
enum Dir {
North,
East,
South,
West,
}
print(`North = {Dir.North}, West = {Dir.West}`)
Run it:
$ scua dir.scua
North = 0, West = 3
You construct a variant through the enum name,
Dir.North, so the reader always sees which type it is. Each
enum is its own type; mixing two different enums is a compile error.
#Payload-free variants are integers
A variant that carries no data is backed by an integer, auto-numbered from 0. That makes enums cheap and lets them index arrays directly, which is handy for per-direction step tables, sprite lookups, or turn order.
You can set explicit discriminants. A bare variant continues from the previous one plus one:
enum Key {
Escape = 27,
Space = 32,
Enter,
}
print(`Escape = {Key.Escape}, Space = {Key.Space}, Enter = {Key.Enter}`)
$ scua key.scua
Escape = 27, Space = 32, Enter = 33
Enter got 33 because it follows
Space = 32.
When you genuinely need to cross the integer boundary, for a C
library, the wire, or a save file, the conversion is explicit.
.to_int() goes out, Enum.from_int(n) comes
back:
enum Dir { North, East, South, West }
print(`East index = {Dir.East.to_int()}`)
let restored = Dir.from_int(2)
print(`from_int(2) is South? {restored == Dir.South}`)
$ scua convert.scua
East index = 1
from_int(2) is South? true
#Variants that carry data
A variant can carry a positional payload. As soon as one does, the whole enum is a tagged sum type: the data rides with the tag, so there's no separate "when it's this state, that field means something" table to keep in sync.
enum Input {
Quit,
Key(int),
Click(int),
}
fn act(e)
match e
Input.Quit -> return "bye"
Input.Key(c) -> return `pressed key {c}`
Input.Click(b) -> return `clicked button {b}`
end
end
print(act(Input.Quit))
print(act(Input.Key(65)))
print(act(Input.Click(2)))
$ scua input.scua
bye
pressed key 65
clicked button 2
A payload can have several fields, and you can name them. The names are documentation. You still match by position:
enum AIState {
Idle,
Chase(target: int, dist: int), -- names are documentation, matched positionally
}
match AIState.Chase(42, 3)
AIState.Idle -> print("idle")
AIState.Chase(t, d) -> print(`chase {t} at range {d}`)
end
$ scua chase.scua
chase 42 at range 3
#Matching and exhaustiveness
match discriminates the variants, and because an enum is
closed, the compiler checks the match is exhaustive. Drop a variant and
it names the gap:
enum Dir { North, East, South, West }
fn label(d)
match d
Dir.North -> return "up"
Dir.East -> return "right"
Dir.South -> return "down"
end
end
print(label(Dir.North))
$ scua label.scua
scua: label.scua:3: type error: match on Dir is not exhaustive — missing: West
That check is the main reason to use an enum over a set of loose constants: add a variant, and every match that needs attention turns into a compile error. See pattern matching for the full set of arm shapes.
#A sharp edge: evolving a persisted enum
There is no versioning or reserve story for enums yet. That matters the moment an enum value is saved or sent over the wire.
Say an older build knows three variants and matches them exhaustively. A newer build adds a fourth and sends a value carrying it. When the old build receives that value and matches on it, none of its arms fit, and it hits a runtime fault:
enum Tool {
Sword,
Bow,
Staff,
}
fn describe(t)
match t
Tool.Sword -> return "melee"
Tool.Bow -> return "ranged"
Tool.Staff -> return "magic"
end
end
-- A value arrives carrying a discriminant this build doesn't know.
let newer = Tool.from_int(3)
print(describe(newer))
$ scua evolve.scua
scua: evolve.scua:10: match: no arm matched (got 3)
at describe (line 10)
at main (line 15)
Until there's a proper versioning mechanism, treat any enum that
crosses a version boundary with care: pin its discriminants, only add
variants at the end, and when you match values that might come from a
newer build, include a _ arm to handle the unknown case
instead of faulting.
#See also
- Pattern matching — matching variants and exhaustiveness.
- Records and gradual types — the other way to shape structured data.
- Errors and faults — what a no-match fault is and how to recover from one.