Table of Contents

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

  1. Define an enum Weekday with all seven days. Write a function is_weekend(d: Weekday) -> bool that returns true for Saturday and Sunday.
  1. Define an enum Coin with variants Penny, Nickel, Dime, Quarter. Write value(c: Coin) -> int that returns the coin's value in cents.
  1. Define enum Temperature { Celsius(int), Fahrenheit(int) }. Write to_celsius(t: Temperature) -> int that converts Fahrenheit to Celsius (use integer math: (f - 32) * 5 / 9).
  1. Create a Result enum and write a safe_divide function that returns Result.Error for division by zero. Chain two divisions: divide 100 by 5, then divide the result by 0.
  1. Build a simple expression evaluator: enum Expr { Num(int), Add(int, int), Mul(int, int), Neg(int) }. Write eval(e: Expr) -> int that computes the result.

Summary

Next chapter: Traits and impl blocks →

← Previous: Closures and first-class functions Next: Traits and impl blocks →