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.
A constructor checks its arguments against the variant’s declaration, the count and each payload’s type. A payload literal is range checked against the field’s declared width too (since 0.5.4), the same bounds an annotation faces, so Tag.V(300) at an int8 field is rejected, literal 300 does not fit in 8 bits, rather than silently truncating.
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. The arms must agree: every value-yielding arm has to produce the same type, so an int32 beside an int64 is rejected, the arms of a match expression must agree on type, rather than the wider value coming back truncated through the first arm’s slot. That agreed type is the match’s own type, so a match bound or passed against an annotation is width and type checked like any other value. Every arm must also yield a value or diverge: an arm that neither yields a tail value nor returns, breaks, or continues is every arm of a match expression must yield a value or return, so no arm reads an uninitialized result. Since 1.10.1 an arm body may itself end in a bare match, which value-izes into the enclosing arm with no name to bind it, so nested matches compose directly.
The arms agree on ownership too, judged with one voice. A match result is an owner only when every value-yielding arm yields an owner, a move of a payload, an alloc, or an owning call; it is a borrow when every arm borrows, and a mix is rejected at the odd arm, the arms of a match expression must agree on ownership of its result. A match over a borrowed subject yields a borrow through every spelling, so a later free of the result, or returning it, is refused the same way a borrowed free is anywhere else. Since 1.9.0 an unannotated x := match ... bind consults that verdict rather than laundering a borrowed result into a freeable binding.
@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.5.4 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,}The result monad is another exported sum type, Result<T, E> = enum { Ok(v: T), Err(e: E) }, added in 0.5.3; see std.functional.
@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. - A method call on an enum value is a compile error (since 0.5.4),
'<name>' is not defined; methods on the enum '<Enum>' are not supported, match on it instead. Only struct receivers dispatch a method.
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