Enums and match
Enums are tagged unions: a value is exactly one of several named variants, each optionally carrying payload data. Like structs, they are a first-class, paradigm-agnostic data type. No @paradigm directive gates their declaration or use. Sum types back the standard library monads (Maybe and Either) and pattern matching.
Declaring an enum
Section titled “Declaring an enum”An enum declaration lists its variants between braces, separated by commas. A variant is either empty or carries named, typed fields in parentheses.
enum Shape { Circle(radius: float64), Rect(w: float64, h: float64), Empty,}An enum declared at the top level of a file can be exported with export enum and imported like any other top-level item; see Source files.
Constructing values
Section titled “Constructing values”A variant is constructed through its enum’s name with dot syntax. Payload arguments are positional, in field declaration order. An empty variant takes no parentheses.
c := Shape.Circle(5.0)r := Shape.Rect(4.0, 6.0)e := Shape.EmptyQualified module paths do not change this: after @import std.functional.maybe, function names are flat but the constructors keep their type name, so you write Maybe.Some(42) and Maybe.None.
Inspecting values with match
Section titled “Inspecting values with match”match takes an enum value (the scrutinee) and a brace-delimited list of arms. Each arm is a pattern, =>, and a body. Exactly one arm runs.
@paradigm procedural
enum Shape { Circle(radius: int64), Rect(w: int64, h: int64), Empty,}
func area(s: Shape) -> int64 { match s { Circle(r) => return r * r * 3, Rect(w, h) => return w * h, Empty => return 0, }}
func main() -> int32 { c := Shape.Circle(5) r := Shape.Rect(4, 6) e := Shape.Empty
println(area(c)) println(area(r)) println(area(e))
return 0}match is defined over enum values only. A scalar or struct scrutinee is a compile error (match needs an enum value, not int64). This rule landed in 0.2.6; before that a non-enum scrutinee executed every arm in sequence.
Arm bodies
Section titled “Arm bodies”An arm body is one of:
- an expression:
Red => 1, - a
returnstatement:Circle(r) => return r * r, - a block:
Red => { println(1) }
Arms are separated by commas. The comma is optional after a block body and after the final arm.
A match whose arms all return on every path counts as returning for the enclosing function, which is why area above needs no trailing return.
Match as an expression
Section titled “Match as an expression”match can appear in expression position, where the chosen arm’s expression is the result.
@paradigm procedural
enum Color { Red, Green, Blue, Custom(code: int64),}
func code(c: Color) -> int64 { x := match c { Red => 1, Green => 2, Custom(n) => n, _ => 0, } return x}
func main() -> int32 { println(code(Color.Red)) println(code(Color.Blue)) println(code(Color.Custom(99))) return 0}Patterns
Section titled “Patterns”Patterns inside match name variants without the enum prefix, written Circle(r), not Shape.Circle(r). There are three pattern forms:
| Pattern | Matches |
|---|---|
Rect(w, h) | The Rect variant, binding one name per payload field in declaration order. |
Empty | The payload-free Empty variant. |
_ | Anything. A wildcard makes the match exhaustive. |
Payload binders are plain names scoped to that arm’s body. Patterns are one level deep: a binder cannot itself be a pattern, and there are no literal patterns or guards.
Exhaustiveness
Section titled “Exhaustiveness”match is exhaustive. A missing variant is a compile error (non exhaustive match, missing variant 'Blue') unless a wildcard arm covers the remainder. Two related checks reject dead arms:
- an arm after a catch-all pattern is
unreachable match arm after a catch all pattern - a repeated variant is
unreachable match arm, 'Red' is already covered
In 0.3.3 the coverage check runs where the scrutinee’s type statically resolves to a known enum, such as an annotated binding or a typed parameter. A scrutinee whose type the checker cannot resolve at that point is not flagged.
Generic enums
Section titled “Generic enums”Generic sum types are written Maybe<T> and are monomorphized at compile time, the same scheme as generic structs and functions (see Types). The standard library’s Maybe is an ordinary exported enum:
export enum Maybe<T> { Some(v: T), None,}@paradigm procedural@import std.functional.maybe
func main() -> int32 { m: Maybe<int64> = Maybe.Some(42) n: Maybe<int64> = Maybe.None
println(unwrap_or(m, 0)) println(unwrap_or(n, 99)) return 0}A recursive enum threads the recursion through a pointer, so each node’s payload stays fixed-size:
enum List<T> { Cons(head: T, tail: *List<T>), Nil,}Layout
Section titled “Layout”An enum value is laid out as a tag (the smallest integer that fits the variant count) plus storage for the largest variant’s payload.
Restrictions
Section titled “Restrictions”matchrequires an enum scrutinee; scalars and structs are rejected.- Printing an enum with
printorprintlnis a compile error (since 0.2.6). Match on it and print the payload instead.
Related pages
Section titled “Related pages”- Types: structs, generics, and monomorphization
- Functional programming: the monad block and
donotation built on sum types - std.functional:
Maybe<T>andEither<L, R> - Error handling: dusk’s
errortype, which is separate from enums