Chapter 8: Enums and Pattern Matching
What you'll learn. Plain enums, enums whose variants carry data, the built-in Option[T] and Result[T, E], the patterns match accepts (literals, bindings, wildcards, guards, tuples, nested variants, union types), how exhaustiveness checking works, and how to write functions over enums.
Why it matters. A web app is full of values that are one of several shapes: a request that is idle, loading, loaded or failed; an order that is in the cart, placed, shipped or cancelled. A status: string field cannot tell you that you forgot a case. An enum can: when you add a variant, the compiler points at every match that does not handle it yet.
If you have used Rust enums, Swift enums or TypeScript discriminated unions, the idea is the same: a closed set of named variants, each with its own payload, and a match the compiler checks for completeness.
Plain enums
The simplest enum is a closed set of names. Unit-only enums are usually written with one variant per line, each ending in ;:
@mode(web.compiled)
enum Direction {
North;
East;
South;
West;
}
func arrow(d: Direction): string {
return match d {
North => "^",
East => ">",
South => "v",
West => "<",
};
}
func main(): void {
let heading = Direction.North;
println(heading);
println(arrow(heading));
println(arrow(Direction.West));
println(Direction.East == Direction.West);
}
Listing 8-1: A plain enum and an exhaustive match
North
^
<
false
A few things to notice:
- A variant is written
Direction.Northwhere a value is expected. Inside amatcharm, the bare nameNorthis enough: the pattern is resolved against the type of the value being matched (Language Reference §1-9.4). The qualified formDirection.North =>works too. matchis an expression. Its arms are separated by commas, each ispattern => result, and there are no parentheses around the value being matched (unlikeifandwhile).printlnrenders an enum value as its variant name.
Mistake: forgetting a variant
Delete the West arm and the program no longer compiles:
enum Direction {
North;
East;
South;
West;
}
func arrow(d: Direction): string {
return match d {
North => "^",
East => ">",
South => "v",
};
}
func main(): void {
println(arrow(Direction.West));
}
main.lpweb:10:12: error[E0001]: Non-exhaustive match: missing variants West
This is the check that makes enums worth using: the message names exactly the variants that are not handled. You can fix it by adding the West arm, or by adding a wildcard arm _ => "?" that matches anything left over. Prefer listing the variants when you can. With a wildcard, a variant added later is silently handled by _; without one, the compiler sends you to every match that needs a decision.
Enums with payloads
A variant can carry data. Variants with payloads are separated by commas, and each payload field has a name and a type:
enum Shape {
Circle(radius: float),
Rect(width: float, height: float),
Point,
}
func area(s: Shape): float {
return match s {
Circle(r) => 3.14159 * r * r,
Rect(w, h) => w * h,
Point => 0.0,
};
}
func main(): void {
let c = Shape.Circle(radius = 2.0);
let r = Shape.Rect(width = 3.0, height = 4.0);
println(c);
println(r);
println(area(c));
println(area(r));
println(area(Shape.Point));
println(r == Shape.Rect(width = 3.0, height = 4.0));
}
Listing 8-2: Variants with payloads, constructed by name and taken apart by match
Circle(2.0)
Rect(3.0, 4.0)
12.56636
12
0
true
You construct a payload variant with named arguments, like a struct (Shape.Rect(width = 3.0, height = 4.0)); positional arguments also work (Shape.Rect(3.0, 4.0)). In a pattern, the payload is bound by position: Rect(w, h) binds w to the width and h to the height. Use _ for a payload you don't need, as in Rect(w, _).
== on enums is structural: two values are equal when they are the same variant with equal payloads. Like structs, enum values are copied on assignment, and their payloads follow the same pure-value rule as struct fields (Chapter 7).
The two variant separators are not mixed: a unit-only enum uses ;, an enum with payloads uses ,. An enum must declare at least one variant (enum Empty {} is E_ENUM_NO_VARIANTS); the type with no values is spelled impossible.
Option[T]: a value that may be missing
Loup has no null. A value that may be absent has type Option[T], a built-in enum with two variants, Some(T) and None. Both names are available without an import.
@mode(web.compiled)
struct User {
id: int;
name: string;
email: Option[string];
}
func findUser(users: list[User], id: int): Option[User] {
for (u in users) {
if (u.id == id) {
return Some(u);
}
}
return None;
}
func main(): void {
let users = [
User(id = 1, name = "Ann", email = Some("ann@example.com")),
User(id = 2, name = "Bo", email = None),
];
let ann = findUser(users, 1);
let nobody = findUser(users, 9);
let greeting = match ann {
Some(u) => "Hello, " + u.name,
None => "Who are you?",
};
println(greeting);
println(nobody?.name ?? "(no such user)");
println(ann.is_some());
println(nobody.is_none());
println(ann.map(func(u: User): int => u.name.length()).unwrap_or(0));
println(repr(nobody));
}
Listing 8-3: Returning and consuming an Option
Hello, Ann
(no such user)
true
true
3
None
The ways to get at the value inside an Option:
matchhandles both cases explicitly. This is the most common form.??supplies a fallback:opt ?? defaultis the value insideSome, ordefaultforNone.?.reads a field or calls a method only when the value is present:nobody?.nameis anOption[string],Nonehere.- Methods:
is_some(),is_none(),unwrap_or(default),map(f),and_then(f),filter(pred),unwrap()andexpect(message).
println renders Some(v) as just v and None as an empty string; repr shows the variant, which is what you want in a log line.
Mistake: the spelling of the methods
The Language Reference and older Loup material write the methods in camelCase (isSome, unwrapOr). The 1.25.1 toolchain implements them in snake_case:
func main(): void {
let port: Option[int] = Some(8080);
if (port.isSome()) {
println("configured");
}
}
main.lpweb:4:9: error[E0001]: type 'Option[int]' has no method 'isSome'
Write port.is_some(). The same goes for is_none, unwrap_or, and the Result methods below.
Mistake: unwrapping None
unwrap() hands back the value inside Some. On None it throws:
struct User {
id: int;
name: string;
}
func findUser(users: list[User], id: int): Option[User] {
for (u in users) {
if (u.id == id) {
return Some(u);
}
}
return None;
}
func main(): void {
let users = [User(id = 1, name = "Ann")];
println(findUser(users, 9).unwrap().name);
}
Uncaught UnwrapError: called `unwrap()` on a None value
at main (main.lpweb:18:13)
unwrap() says "I know this is present". When you don't know that, handle the None case:
struct User {
id: int;
name: string;
}
func findUser(users: list[User], id: int): Option[User] {
for (u in users) {
if (u.id == id) {
return Some(u);
}
}
return None;
}
func main(): void {
let users = [User(id = 1, name = "Ann")];
println(findUser(users, 9)?.name ?? "guest");
}
guest
expect("why it must be there") is unwrap() with your own message, for the cases where absence really is a bug.
Known gap (1.25.1). The
Optionreturned bydict.get(key)works withmatch,??,is_some(),unwrap()andunwrap_or(), butmapon it fails at run time andreprprints the bare value instead ofSome(…). Usematchor??on adict.getresult.
Result[T, E]: success or a described failure
Result[T, E] is the other built-in enum: Ok(T) for success, Err(E) for failure. Use it when a failure is an expected outcome the caller should decide about, such as invalid input or an insufficient balance.
@mode(web.compiled)
struct Account {
owner: string;
balanceCents: int;
}
func withdraw(acct: Account, cents: int): Result[Account, string] {
if (cents <= 0) {
return Err("amount must be positive");
}
if (cents > acct.balanceCents) {
return Err(acct.owner + ": insufficient funds");
}
return Ok(acct with (balanceCents = acct.balanceCents - cents));
}
func deposit(acct: Account, cents: int): Result[Account, string] {
if (cents <= 0) {
return Err("amount must be positive");
}
return Ok(acct with (balanceCents = acct.balanceCents + cents));
}
func transfer(source: Account, target: Account, cents: int): Result[(Account, Account), string] {
let newSource = withdraw(source, cents)?;
let newTarget = deposit(target, cents)?;
return Ok((newSource, newTarget));
}
func main(): void {
let ann = Account(owner = "ann", balanceCents = 5000);
let bo = Account(owner = "bo", balanceCents = 1000);
match transfer(ann, bo, 1500) {
Ok((a, b)) => println(str(a.balanceCents) + " / " + str(b.balanceCents)),
Err(msg) => println("failed: " + msg),
}
match transfer(bo, ann, 9999) {
Ok((a, b)) => println(str(a.balanceCents) + " / " + str(b.balanceCents)),
Err(msg) => println("failed: " + msg),
}
let r = withdraw(ann, 700);
println(r.is_ok());
println(r.map(func(a: Account): int => a.balanceCents).unwrap_or(0));
println(withdraw(ann, -5).unwrap_err());
}
Listing 8-4: Functions that return Result, and ? to pass an Err up
3500 / 2500
failed: bo: insufficient funds
true
4300
amount must be positive
The postfix ? in transfer is the propagation operator (Language Reference §1-5.7). withdraw(source, cents)? is the Account inside Ok; if the result is an Err, transfer returns that Err at once. ? is allowed only in a function that itself returns a Result.
The Result methods implemented today are is_ok(), is_err(), unwrap(), expect(message), unwrap_or(default), unwrap_err(), map(f), map_err(f) and and_then(f). Chapter 12 compares Result with exceptions and covers when to use each.
The arm pattern Ok((a, b)) takes the tuple apart inside the Ok; nested patterns are covered below.
Mistake: from as a parameter name
A first draft of transfer would naturally say transfer(from: Account, to: Account, …):
struct Account {
owner: string;
balanceCents: int;
}
func transfer(from: Account, to: Account, cents: int): Result[int, string] {
return Ok(cents);
}
func main(): void {
println("never runs");
}
main.lpweb:7:15: error[E0001]: `from` is a reserved word (§1-3.4) and cannot name a parameter
note: rename the parameter — e.g. `from_value`
from is reserved because it starts an import (from core.math import (sqrt);). Rename the parameter, as Listing 8-4 does with source and target.
Mistake: unwrapOr compiles, then fails
On Result, the camelCase spelling is not caught by the compiler. It fails when the call runs:
func withdraw(balance: int, amount: int): Result[int, string] {
if (amount > balance) {
return Err("insufficient funds");
}
return Ok(balance - amount);
}
func main(): void {
let left = withdraw(100, 250).unwrapOr(100);
println(left);
}
Uncaught RuntimeError: internal interpreter limitation: unsupported instruction: CallVirtual unwrapOr on user class Result — method not found
The message says the interpreter could not find a method called unwrapOr on Result. The fix is the snake_case name, unwrap_or(100). Because this mistake gets past the compiler, search your code for unwrapOr, isOk and isErr when you port examples from older material.
Mistake: ? on an Option
? works on Result, not on Option:
func firstEven(xs: list[int]): Option[int] {
for (x in xs) {
if (x % 2 == 0) {
return Some(x);
}
}
return None;
}
func doubledFirstEven(xs: list[int]): Option[int] {
let n = firstEven(xs)?;
return Some(n * 2);
}
func main(): void {
println(doubledFirstEven([1, 4, 5]));
}
main.lpweb:12:13: error[E_PROPAGATE_WRONG_RETURN]: E_PROPAGATE_WRONG_RETURN: the '?' operator can only be used inside a function returning Result (§1-5.7)
For an Option, use map, and_then, ?? or a match. (Inside a function that returns Result, the 1.25.1 compiler does accept ? on an Option and turns a None into Err(None). The Language Reference does not allow this, so don't rely on it.)
func firstEven(xs: list[int]): Option[int] {
for (x in xs) {
if (x % 2 == 0) {
return Some(x);
}
}
return None;
}
func doubledFirstEven(xs: list[int]): Option[int] {
return firstEven(xs).map(func(n: int): int => n * 2);
}
func main(): void {
println(repr(doubledFirstEven([1, 4, 5])));
println(repr(doubledFirstEven([1, 3])));
println(firstEven([1, 3]) ?? -1);
}
Some(8)
None
-1
match in depth
Literal patterns and wildcards
A literal pattern matches an equal value. _ matches anything and binds nothing:
func command(word: string): string {
return match word {
"start" => "starting",
"stop" => "stopping",
_ => "unknown command: " + word,
};
}
func main(): void {
println(command("stop"));
println(command("pause"));
}
stopping
unknown command: pause
A match over an int or a string has no fixed set of variants, so it needs a _ arm (or a binding arm) to be exhaustive.
Binding patterns and guards
A plain name in a pattern binds the value. A guard, if (condition) after the pattern, adds a condition the arm must also satisfy:
func sizeLabel(bytes: Option[int]): string {
return match bytes {
Some(n) if (n >= 1000000) => i"{$ n // 1000000 $} MB",
Some(n) if (n >= 1000) => i"{$ n // 1000 $} KB",
Some(n) => i"{$ n $} bytes",
None => "size unknown",
};
}
func main(): void {
println(sizeLabel(Some(2500000)));
println(sizeLabel(Some(4096)));
println(sizeLabel(Some(12)));
println(sizeLabel(None));
}
Listing 8-5: Guards choose between arms that match the same variant
2 MB
4 KB
12 bytes
size unknown
Arms are tried from top to bottom, and the first arm whose pattern and guard both match wins. For exhaustiveness the compiler treats a guarded arm as one that might not match, which is why Some(n) => without a guard is still needed.
Known gap: a guarded binding next to literal arms
Mapping HTTP status codes to text mixes literal arms with a guarded binding. In 1.25.1 that combination misbehaves:
func statusText(code: int): string {
return match code {
200 => "OK",
404 => "Not Found",
n if (n >= 500) => i"server error {$ n $}",
n => i"status {$ n $}",
};
}
func main(): void {
println(statusText(503));
println(statusText(418));
}
server error
status 418
The guard selects the right arm, but in a match that also has literal arms, the name bound by a guarded arm renders empty in the arm's body. This is an interpreter bug, not something your code did wrong. Until it is fixed, guard on the value you are matching instead of on a new binding:
func statusText(code: int): string {
return match code {
200 => "OK",
404 => "Not Found",
_ if (code >= 500) => i"server error {$ code $}",
n => i"status {$ n $}",
};
}
func main(): void {
println(statusText(200));
println(statusText(404));
println(statusText(503));
println(statusText(418));
}
Listing 8-6: Literal arms, a guarded wildcard and a binding arm
OK
Not Found
server error 503
status 418
Tuple patterns
Matching a tuple lets one match look at several values at once. Tuple patterns may contain literals, bindings, _ and nested tuples:
func quadrant(p: (int, int)): string {
return match p {
(0, 0) => "origin",
(0, _) => "on the y axis",
(_, 0) => "on the x axis",
_ if (p[0] > 0 and p[1] > 0) => "first quadrant",
_ => "elsewhere",
};
}
func main(): void {
println(quadrant((0, 0)));
println(quadrant((0, 5)));
println(quadrant((3, 4)));
println(quadrant((-1, 4)));
}
origin
on the y axis
first quadrant
elsewhere
Enum variant patterns are not allowed inside a tuple pattern in 1.25.1; the worked example at the end of the chapter shows what to do instead.
Nested patterns
A pattern can look inside a variant's payload. Arms are tried in order and an arm matches only if its whole pattern matches, so Ok(Some(n)) and Ok(None) are different cases:
func describe(r: Result[Option[int], string]): string {
return match r {
Ok(Some(n)) => i"value {$ n $}",
Ok(None) => "no value",
Err(e) => "error: " + e,
};
}
func main(): void {
println(describe(Ok(Some(5))));
println(describe(Ok(None)));
println(describe(Err("timeout")));
}
value 5
no value
error: timeout
Mistake: one arm for two variants
Rust and TypeScript let one arm cover several cases with |. Loup does not (Language Reference §1-9.4: each arm has exactly one pattern):
enum Direction {
North;
East;
South;
West;
}
func axis(d: Direction): string {
return match d {
North | South => "vertical",
East | West => "horizontal",
};
}
func main(): void {
println(axis(Direction.West));
}
main.lpweb:11:15: error[E0001]: expected FatArrow, found Pipe
main.lpweb:11:15: error[E0001]: expected expression
main.lpweb:12:14: error[E0001]: expected FatArrow, found Pipe
main.lpweb:12:14: error[E0001]: expected expression
The parser expected the => after the first pattern and found |. Write one arm per variant:
enum Direction {
North;
East;
South;
West;
}
func axis(d: Direction): string {
return match d {
North => "vertical",
South => "vertical",
East => "horizontal",
West => "horizontal",
};
}
func main(): void {
println(axis(Direction.West));
}
horizontal
When many variants share a result, a guard over a helper function (_ if (isVertical(d)) => …) also works.
Block arms and match as a statement
An arm's result can be a block. The block's last expression is the arm's value. When you only want side effects, use match as a statement:
enum Shape {
Circle(radius: float),
Rect(width: float, height: float),
Point,
}
func perimeter(s: Shape): float {
return match s {
Circle(r) => {
let pi = 3.14159;
2.0 * pi * r
},
Rect(w, h) => 2.0 * (w + h),
Point => 0.0,
};
}
func main(): void {
println(perimeter(Shape.Circle(radius = 1.0)));
let shapes = [Shape.Circle(radius = 2.0), Shape.Point, Shape.Rect(width = 1.0, height = 1.0)];
for (s in shapes) {
match s {
Circle(r) => println("circle " + str(r)),
Rect(w, h) => println("rect " + str(w) + "x" + str(h)),
Point => println("point"),
}
}
}
6.28318
circle 2
point
rect 1x1
Matching union types
A value of a union type such as int | string | bool (Chapter 3) is matched by type. The pattern int(n) matches an int and binds it:
func describe(v: int | string | bool): string {
return match v {
int(n) => i"int {$ n $}",
string(s) => "string " + s,
bool(b) => i"bool {$ b $}",
};
}
func label(v: string | int): string {
if (v is string) {
return "text: " + v.toUpperCase();
}
return "number";
}
func main(): void {
println(describe(42));
println(describe("hi"));
println(describe(true));
println(label("ok"));
println(label(3));
}
int 42
string hi
bool true
text: OK
number
The match must cover every member of the union; leaving out bool is Non-exhaustive match: missing types bool. For a single check, is narrows the type inside the if branch, so v.toUpperCase() is allowed there.
Generic enums
An enum can take type parameters, and a variant can refer to the enum itself. That makes trees easy to express:
enum Tree[T] {
Leaf(value: T),
Node(left: Tree[T], right: Tree[T]),
}
func countLeaves[T](t: Tree[T]): int {
return match t {
Leaf(_) => 1,
Node(l, r) => countLeaves(l) + countLeaves(r),
};
}
func sumTree(t: Tree[int]): int {
return match t {
Leaf(v) => v,
Node(l, r) => sumTree(l) + sumTree(r),
};
}
func main(): void {
let t = Tree.Node(
left = Tree.Leaf(value = 1),
right = Tree.Node(left = Tree.Leaf(value = 2), right = Tree.Leaf(value = 3)),
);
println(countLeaves(t));
println(sumTree(t));
}
Listing 8-7: A recursive generic enum
3
6
Option[T] and Result[T, E] are generic enums of exactly this kind. Chapter 11 covers generics in general.
Functions over enums
In Rust or Swift you would put a typeName() method inside the enum. In Loup, an enum body holds only variants (the grammar in Language Reference §1-19 has no other member):
enum Json {
Null,
Number(value: float),
Text(value: string),
func typeName(): string {
return "?";
}
}
func main(): void {
println("never runs");
}
main.lpweb:7:5: error[E0001]: `func` is a reserved word (§1-3.4) and cannot name an enum variant
note: rename the enum variant — e.g. `func_value`
main.lpweb:7:10: error[E0002]: expected `}`
main.lpweb:7:20: error[E0003]: expected `;`
main.lpweb:9:5: error[E0001]: expected expression
main.lpweb:10:1: error[E0001]: expected expression
The parser read func as the name of a fourth variant, which is why the first message talks about a reserved word. Write the operation as a module-level function that takes the enum:
enum Json {
Null,
Bool(value: bool),
Number(value: float),
Text(value: string),
Array(items: list[Json]),
}
func typeName(v: Json): string {
return match v {
Null => "null",
Bool(_) => "bool",
Number(_) => "number",
Text(_) => "string",
Array(_) => "array",
};
}
func countScalars(v: Json): int {
return match v {
Array(items) => {
var n = 0;
for (item in items) {
n = n + countScalars(item);
}
n
},
_ => 1,
};
}
func main(): void {
let doc = Json.Array(items = [
Json.Number(value = 1.5),
Json.Text(value = "hi"),
Json.Array(items = [Json.Bool(value = true), Json.Null]),
]);
println(typeName(doc));
println(countScalars(doc));
println(doc);
}
Listing 8-8: A recursive Json enum with functions that match over it
array
4
Array([Number(1.5), Text("hi"), Array([Bool(true), Null])])
Adding a variant to Json makes typeName fail to compile until it handles the new case. countScalars has a _ arm, so it would accept the new variant silently; that is the trade-off from the start of the chapter.
A worked example: an order state machine
An online order moves through states in response to events. Some transitions are allowed (a placed order can be shipped) and some are not (a shipped order cannot be cancelled). Two enums and a function that returns a Result describe the whole thing.
The first attempt matches on the pair (state, event):
enum OrderState {
Cart,
Placed(orderId: string),
}
enum OrderEvent {
Checkout(orderId: string),
Cancel(reason: string),
}
func next(state: OrderState, event: OrderEvent): Result[OrderState, string] {
return match (state, event) {
(Cart, Checkout(id)) => Ok(OrderState.Placed(orderId = id)),
_ => Err("not allowed"),
};
}
func main(): void {
println("never runs");
}
main.lpweb:14:10: error[E0001]: unsupported pattern inside a tuple pattern (only literals, bindings, `_` and nested tuples are supported)
main.lpweb:14:16: error[E0001]: unsupported pattern inside a tuple pattern (only literals, bindings, `_` and nested tuples are supported)
In 1.25.1 a tuple pattern cannot contain variant patterns. Match on the state first, then on the event inside each arm:
@mode(web.compiled)
enum OrderState {
Cart,
Placed(orderId: string),
Shipped(orderId: string, tracking: string),
Delivered(orderId: string),
Cancelled(reason: string),
}
enum OrderEvent {
Checkout(orderId: string),
Ship(tracking: string),
Deliver,
Cancel(reason: string),
}
func rejected(state: OrderState, event: OrderEvent): Result[OrderState, string] {
return Err("cannot apply " + str(event) + " to " + str(state));
}
func next(state: OrderState, event: OrderEvent): Result[OrderState, string] {
return match state {
Cart => match event {
Checkout(id) => Ok(OrderState.Placed(orderId = id)),
Cancel(why) => Ok(OrderState.Cancelled(reason = why)),
_ => rejected(state, event),
},
Placed(id) => match event {
Ship(tracking) => Ok(OrderState.Shipped(orderId = id, tracking = tracking)),
Cancel(why) => Ok(OrderState.Cancelled(reason = why)),
_ => rejected(state, event),
},
Shipped(id, _) => match event {
Deliver => Ok(OrderState.Delivered(orderId = id)),
_ => rejected(state, event),
},
_ => rejected(state, event),
};
}
func run(events: list[OrderEvent]): Result[OrderState, string] {
var state = OrderState.Cart;
for (e in events) {
state = next(state, e)?;
}
return Ok(state);
}
func describe(result: Result[OrderState, string]): string {
return match result {
Ok(Delivered(id)) => "order " + id + " delivered",
Ok(other) => "order is now " + str(other),
Err(msg) => "rejected: " + msg,
};
}
func main(): void {
let happy = [
OrderEvent.Checkout(orderId = "A-17"),
OrderEvent.Ship(tracking = "1Z999"),
OrderEvent.Deliver,
];
let late = [
OrderEvent.Checkout(orderId = "A-18"),
OrderEvent.Ship(tracking = "1Z123"),
OrderEvent.Cancel(reason = "changed mind"),
];
let early = [
OrderEvent.Checkout(orderId = "A-19"),
OrderEvent.Cancel(reason = "duplicate"),
];
println(describe(run(happy)));
println(describe(run(late)));
println(describe(run(early)));
}
Listing 8-9: An order state machine: enums for states and events, Result for rejected transitions
order A-17 delivered
rejected: cannot apply Cancel("changed mind") to Shipped("A-18", "1Z123")
order is now Cancelled("duplicate")
The pieces from this chapter fit together here:
nextis amatchinside amatch. Each state lists the events it accepts; everything else goes torejected.runapplies events in order and uses?to stop at the first rejected transition.describeuses a nested pattern,Ok(Delivered(id)), before the more generalOk(other).str(event)andstr(state)render enum values the same wayprintlndoes, which gives readable error messages for free.
The same shape (an enum of states, an enum of events, a function from one state to the next) is how you would model a form wizard or a fetch lifecycle in a web UI.
Try it yourself
- Add a
Returned(orderId: string)state that aDeliveredorder can move to on a newReturnevent. Compile before you updatenextanddescribe; whichmatchexpressions does the compiler point at, and which ones does it not, because of a_arm? - Model a fetch lifecycle:
enum Fetch[T] { Idle, Loading, Loaded(value: T), Failed(message: string) }, and writefunc label(f: Fetch[int]): string. - Write
func depth(v: Json): intfor theJsonenum in Listing 8-8.
Summary
- An enum is a closed set of variants. Unit-only enums use
;between variants; enums with payloads use,. - Construct a variant as
Shape.Rect(width = 3.0, height = 4.0); take it apart withRect(w, h) =>in amatch. Enum==is structural. matchmust be exhaustive. The compiler names the missing variants or union members; a_arm covers the rest, at the cost of not being told about new variants.- Patterns: literals, bindings,
_, guards (if (…)), tuples, nested variants such asOk(Some(n)), and type patterns such asint(n)for unions. There are no|alternatives in an arm. Option[T]replacesnull; usematch,??,?.and the snake_case methods (is_some,unwrap_or,map, …).unwrap()onNonethrowsUnwrapError.Result[T, E]carries a success or a failure;?passes anErrup from a function that returnsResult, and does not apply toOption.- Enum bodies hold only variants; write operations as functions that take the enum.
- Known gaps in 1.25.1: camelCase
Resultmethods such asunwrapOrcompile and then fail at run time; a guarded binding in amatchthat also has literal arms renders empty; variant patterns cannot appear inside tuple patterns;mapandreprmisbehave on adict.getresult.