Enums and pattern matching
What is an enum?
Sometimes a value can only be one of a few specific options. A traffic light is Red, Yellow, or Green — never anything else. A playing card suit is Hearts, Diamonds, Clubs, or Spades — exactly four possibilities.
An enum (short for "enumeration") lets you define a type that is exactly one of a fixed set of variants.
Defining a simple enum
enum Color { Red, Green, Blue } fn main() { let c: Color = Color.Red print(c) // Red }
Color is a new type with exactly three possible values. You refer to each variant with a dot: Color.Red, Color.Green, Color.Blue.
Pattern matching with match
The real power of enums comes from match — a way to handle each variant differently:
enum Direction { North, South, East, West } fn describe(d: Direction) -> String { return match d { Direction.North => "Going up", Direction.South => "Going down", Direction.East => "Going right", Direction.West => "Going left" } } fn main() { print(describe(Direction.North)) // Going up print(describe(Direction.West)) // Going left }
match checks which variant d is, and returns the corresponding value. It is like a series of if checks, but cleaner and the compiler ensures you handle every case.
The wildcard pattern
If you do not care about some variants, use _ as a catch-all:
enum Color { Red, Green, Blue, Yellow, Purple } fn is_primary(c: Color) -> bool { return match c { Color.Red => true, Color.Green => true, Color.Blue => true, _ => false } } fn main() { print(is_primary(Color.Red)) // true print(is_primary(Color.Yellow)) // false }
The _ matches any variant not explicitly listed.
Enums with data
Simple enums are useful, but enums become truly powerful when variants carry data:
enum Shape { Circle(int), Rectangle(int, int), Triangle(int, int, int) }
Each variant can hold different types and amounts of data. A Circle has a radius, a Rectangle has width and height, a Triangle has three sides.
fn area(s: Shape) -> int { return match s { Shape.Circle(r) => 3 * r * r, Shape.Rectangle(w, h) => w * h, Shape.Triangle(a, b, c) => { // Heron's formula approximation using integer math let s: int = (a + b + c) / 2 return s * (s - a) / 2 } } } fn describe(s: Shape) -> String { return match s { Shape.Circle(r) => "Circle with radius " + int_to_string(r), Shape.Rectangle(w, h) => "Rectangle " + int_to_string(w) + "x" + int_to_string(h), Shape.Triangle(a, b, c) => "Triangle with sides " + int_to_string(a) + ", " + int_to_string(b) + ", " + int_to_string(c) } } fn main() { let shapes: Array = [ Shape.Circle(10), Shape.Rectangle(5, 8), Shape.Triangle(3, 4, 5) ] var i: int = 0 while i < shapes.length() { let s: Shape = shapes[i] print(describe(s) + " -> area = " + int_to_string(area(s))) i += 1 } }
Output:
Circle with radius 10 -> area = 300 Rectangle 5x8 -> area = 40 Triangle with sides 3, 4, 5 -> area = 3
Enums for error handling
One of the most common uses of enums is representing success or failure:
enum Result { Ok(int), Error(String) } fn divide(a: int, b: int) -> Result { if b == 0 { return Result.Error("Division by zero") } return Result.Ok(a / b) } fn main() { let r1: Result = divide(10, 3) let r2: Result = divide(10, 0) let msg1: String = match r1 { Result.Ok(value) => "Result: " + int_to_string(value), Result.Error(msg) => "Error: " + msg } let msg2: String = match r2 { Result.Ok(value) => "Result: " + int_to_string(value), Result.Error(msg) => "Error: " + msg } print(msg1) // Result: 3 print(msg2) // Error: Division by zero }
Instead of crashing on bad input, the function returns an enum that the caller must handle. This makes errors explicit and impossible to ignore.
Enums for optional values
Another common pattern — representing "something or nothing":
enum Option { Some(int), None } fn find_index(arr: Array, target: int) -> Option { var i: int = 0 while i < arr.length() { if arr[i] == target { return Option.Some(i) } i += 1 } return Option.None } fn main() { let nums: Array = [10, 20, 30, 40, 50] let found: Option = find_index(nums, 30) let not_found: Option = find_index(nums, 99) let msg1: String = match found { Option.Some(idx) => "Found at index " + int_to_string(idx), Option.None => "Not found" } let msg2: String = match not_found { Option.Some(idx) => "Found at index " + int_to_string(idx), Option.None => "Not found" } print(msg1) // Found at index 2 print(msg2) // Not found }
Using enums as state machines
Enums naturally represent states:
enum OrderStatus { Pending, Processing, Shipped(String), Delivered, Cancelled(String) } fn describe_order(status: OrderStatus) -> String { return match status { OrderStatus.Pending => "Order is pending", OrderStatus.Processing => "Order is being processed", OrderStatus.Shipped(tracking) => "Shipped! Tracking: " + tracking, OrderStatus.Delivered => "Order delivered", OrderStatus.Cancelled(reason) => "Cancelled: " + reason } } fn main() { let s1: OrderStatus = OrderStatus.Pending let s2: OrderStatus = OrderStatus.Shipped("NYX-12345") let s3: OrderStatus = OrderStatus.Cancelled("Out of stock") print(describe_order(s1)) // Order is pending print(describe_order(s2)) // Shipped! Tracking: NYX-12345 print(describe_order(s3)) // Cancelled: Out of stock }
Practical example: a calculator
enum Operation { Add(int, int), Subtract(int, int), Multiply(int, int), Divide(int, int) } fn calculate(op: Operation) -> int { return match op { Operation.Add(a, b) => a + b, Operation.Subtract(a, b) => a - b, Operation.Multiply(a, b) => a * b, Operation.Divide(a, b) => a / b } } fn show(op: Operation) -> String { return match op { Operation.Add(a, b) => int_to_string(a) + " + " + int_to_string(b), Operation.Subtract(a, b) => int_to_string(a) + " - " + int_to_string(b), Operation.Multiply(a, b) => int_to_string(a) + " * " + int_to_string(b), Operation.Divide(a, b) => int_to_string(a) + " / " + int_to_string(b) } } fn main() { let ops: Array = [ Operation.Add(10, 5), Operation.Subtract(10, 5), Operation.Multiply(10, 5), Operation.Divide(10, 5) ] var i: int = 0 while i < ops.length() { let op: Operation = ops[i] print(show(op) + " = " + int_to_string(calculate(op))) i += 1 } }
Output:
10 + 5 = 15 10 - 5 = 5 10 * 5 = 50 10 / 5 = 2
Exercises
- Define an enum
Weekdaywith all seven days. Write a functionis_weekend(d: Weekday) -> boolthat returnstruefor Saturday and Sunday.
- Define an enum
Coinwith variantsPenny,Nickel,Dime,Quarter. Writevalue(c: Coin) -> intthat returns the coin's value in cents.
- Define
enum Temperature { Celsius(int), Fahrenheit(int) }. Writeto_celsius(t: Temperature) -> intthat converts Fahrenheit to Celsius (use integer math:(f - 32) * 5 / 9).
- Create a
Resultenum and write asafe_dividefunction that returnsResult.Errorfor division by zero. Chain two divisions: divide 100 by 5, then divide the result by 0.
- Build a simple expression evaluator:
enum Expr { Num(int), Add(int, int), Mul(int, int), Neg(int) }. Writeeval(e: Expr) -> intthat computes the result.
Summary
enum Name { Variant1, Variant2, ... }defines a type with fixed variants.- Access variants with dot notation:
Color.Red. - Variants can carry data:
Shape.Circle(10). matchinspects which variant a value is and extracts its data._is a wildcard that matches any unhandled variant.- Common patterns:
Result(Ok/Error) for error handling,Option(Some/None) for optional values. - Enums + match = clean, safe handling of multiple cases.
Next chapter: Traits and impl blocks →