Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

8. Enums and Pattern Matching

An enum in C is a set of names for integers. An enum in Swift is a type whose value is one of a fixed set of cases, and a case may carry values of its own. That second half turns it into something C has no word for and Lux calls a sum type: a value that is one shape or another, never both, never neither.

This is the feature Swift is built around. Optional is one. Result is one. Most of the types worth defining in a program turn out to be one.

8.1 Cases and Raw Values

The simple form names a set of alternatives, and the type may be given a raw value so that it can cross the boundary into a file or a network.

// An enum with raw values is a name for a value the outside world uses.
enum Level: Int, CaseIterable {
    case debug = 10
    case info = 20
    case warning = 30
    case error = 40

    var label: String {
        "\(self)".uppercased()
    }

    var isNoisy: Bool {
        self <= .info
    }
}

extension Level: Comparable {
    static func < (a: Level, b: Level) -> Bool {
        a.rawValue < b.rawValue
    }
}

for level in Level.allCases {
    print(level.rawValue, level.label, level.isNoisy)
}

// Coming back from a raw value can fail, so it hands back an optional.
print(Level(rawValue: 30)?.label ?? "unknown")
print(Level(rawValue: 35)?.label ?? "unknown")
10 DEBUG true
20 INFO true
30 WARNING false
40 ERROR false
WARNING
unknown

Several things are worth taking from that.

An enum can have methods and computed properties, which surprises people who arrive expecting a list of constants. isNoisy belongs on the type because it is a fact about levels, and putting it there means it cannot drift out of step with a copy of the same logic somewhere else.

CaseIterable gives you allCases, in declaration order, for free. The compiler writes it, and it stays right when you add a case.

The raw value is not the case. Level.warning is a Level; 30 is an Int that happens to represent it. Going from the case to the raw value always works and is written .rawValue; coming back may fail, so Level(rawValue:) is a failable initializer returning Level?. Strings work as raw values too, and with no values given, the raw value of each case is its own name, which is how most parsing enums are written.

Inside the type, .debug may be written without the type name wherever the type is already known, which is why self <= .info reads as it does.

8.2 Associated Values

A case may carry values, and different cases may carry different ones. That is what makes an enum a type rather than a list of names.

// One type, several shapes, each carrying only what its own case needs.
enum Command {
    case quit
    case move(direction: String, steps: Int)
    case say(String)
    case unknown(String)
}

func parse(_ line: String) -> Command {
    let parts = line.split(separator: " ").map(String.init)
    switch parts.first {
    case "quit", "q":
        return .quit
    case "go":
        guard parts.count == 3, let steps = Int(parts[2]) else {
            return .unknown(line)
        }
        return .move(direction: parts[1], steps: steps)
    case "say":
        return .say(parts.dropFirst().joined(separator: " "))
    default:
        return .unknown(line)
    }
}

func run(_ c: Command) -> String {
    switch c {
    case .quit:
        return "bye"
    case .move(let direction, let steps) where steps > 10:
        return "that is too far to go \(direction)"
    case .move(let direction, let steps):
        return "going \(direction) \(steps) step\(steps == 1 ? "" : "s")"
    case .say(let text) where text.isEmpty:
        return "you say nothing"
    case .say(let text):
        return "you say \"\(text)\""
    case .unknown(let line):
        return "I do not know how to \"\(line)\""
    }
}

for line in ["go north 3", "go north 1", "go up 99", "say hello there", "say", "dance", "q"] {
    print(run(parse(line)))
}
going north 3 steps
going north 1 step
that is too far to go up
you say "hello there"
you say nothing
I do not know how to "dance"
bye

Command is one type with four shapes. A .quit carries nothing, a .move carries a direction and a count, a .say carries a line of text. There is no Command with a steps and no direction, no Command that is both a move and a quit, and no field that is meaningful for one case and ignored for the others.

Compare the alternative. A struct with an optional direction, an optional step count, an optional message and a kind tag can represent all of those nonsense combinations, and every function reading it has to decide what to do about them. The enum makes the nonsense unspellable, which means nobody has to write the code that handles it.

switch over the enum is exhaustive, so when you add a .look case next month the compiler stops at every function that has to change and names the file and line. That is the argument for this design in one sentence. A missed case is a compile error rather than a bug report.

Note parts.first in parse: it is a String?, and the switch matches it against string literals directly, because an optional is an enum and a literal is a pattern for the .some case. The default catches nil along with everything else.

Exercise 8-1. Add a .look case to Command and let the compiler show you every place that needs changing.

Exercise 8-2. Rewrite Command as a struct with optional fields, write the same run function against it, and count the impossible states you had to handle.

8.3 Recursive Enums

A case cannot normally hold a value of its own type, since that would make the size of the type infinite. indirect puts a level of pointer between them and the problem goes away.

// A tiny expression tree. `indirect` lets a case hold its own type.
indirect enum Expr {
    case number(Double)
    case add(Expr, Expr)
    case subtract(Expr, Expr)
    case multiply(Expr, Expr)
    case divide(Expr, Expr)
    case negate(Expr)
}

func evaluate(_ e: Expr) -> Double {
    switch e {
    case .number(let n):
        return n
    case .add(let a, let b):
        return evaluate(a) + evaluate(b)
    case .subtract(let a, let b):
        return evaluate(a) - evaluate(b)
    case .multiply(let a, let b):
        return evaluate(a) * evaluate(b)
    case .divide(let a, let b):
        return evaluate(a) / evaluate(b)
    case .negate(let a):
        return -evaluate(a)
    }
}

func show(_ e: Expr) -> String {
    switch e {
    case .number(let n):
        if let whole = Int(exactly: n) { return "\(whole)" }
        return "\(n)"
    case .add(let a, let b):
        return "(\(show(a)) + \(show(b)))"
    case .subtract(let a, let b):
        return "(\(show(a)) - \(show(b)))"
    case .multiply(let a, let b):
        return "(\(show(a)) * \(show(b)))"
    case .divide(let a, let b):
        return "(\(show(a)) / \(show(b)))"
    case .negate(let a):
        return "-\(show(a))"
    }
}

// (2 + 3) * -(10 / 4)
let e = Expr.multiply(
    .add(.number(2), .number(3)),
    .negate(.divide(.number(10), .number(4)))
)

print(show(e), "=", evaluate(e))
print(show(.add(.number(1), .multiply(.number(2), .number(3)))))
((2 + 3) * -(10 / 4)) = -12.5
(1 + (2 * 3))

That is an interpreter for arithmetic in forty lines, and the shape generalizes: a tree is an enum with a leaf case and a branch case, a JSON value is an enum with six cases, a linked list is an enum with empty and node. Two functions walk the same tree without either of them knowing about the other, which is what you get when the data is separate from the operations on it.

indirect may be written on a single case or on the whole enum, as here. The cost is one allocation per node, which is what a tree costs in any language.

Exercise 8-3. Add a .power(Expr, Expr) case and let the compiler find the two functions that need it.

Exercise 8-4. Write depth(_:) returning how deeply nested an Expr is.

Exercise 8-5. Write a JSON enum with cases for null, boolean, number, string, array and object, and a function that renders one as text.

8.4 Patterns Outside switch

The patterns a switch arm uses work in several other places, which saves writing a switch with one real arm and a default that does nothing.

// Patterns are not only for switch.
enum Event {
    case click(x: Int, y: Int)
    case key(Character)
    case scroll(Int)
}

let events: [Event] = [
    .click(x: 3, y: 4), .key("a"), .scroll(-2), .key("q"), .click(x: 5, y: 5),
]

// if case tests a single pattern.
if case .click(let x, let y) = events[0] {
    print("first click at \(x),\(y)")
}

// for case keeps only the elements that match.
for case .key(let c) in events {
    print("key", c)
}

// and it composes with where.
for case .click(let x, let y) in events where x == y {
    print("click on the diagonal at \(x),\(y)")
}

// guard case is the early-exit form.
func firstScroll(_ events: [Event]) -> Int? {
    for e in events {
        guard case .scroll(let amount) = e else { continue }
        return amount
    }
    return nil
}
print("first scroll:", firstScroll(events) ?? 0)

// Optional patterns work the same way, because Optional is an enum.
let maybes: [Int?] = [1, nil, 3, nil, 5]
for case let n? in maybes {
    print("value", n)
}
first click at 3,4
key a
key q
click on the diagonal at 5,5
first scroll: -2
value 1
value 3
value 5

for case let n? in maybes is the idiom for walking a collection of optionals and skipping the empty ones. It is compactMap said as a loop, and when the body does real work it reads better.

The ~= operator is what all of this is built on, and you can give it an overload for your own type to make your own values matchable in a case. That is rarely needed and is worth knowing exists.

8.5 Enum or Class Hierarchy

Both model “one of several kinds of thing” and they fail in opposite directions.

An enum fixes the set of cases and makes adding an operation easy: write a new function with a switch and you are done. Adding a case is the expensive move, because every existing switch must be revisited, and the compiler makes you do it.

A class hierarchy fixes the set of operations and makes adding a kind easy: write a new subclass and implement the methods. Adding an operation is the expensive move, because every existing class must grow a method, and nothing makes you do it, so one of them will be missed.

Choose by which axis moves. A set of commands, a set of token types, a set of states: the cases are known and the operations keep coming, so use an enum. A set of plugins, a set of shapes a drawing program can be extended with: the operations are known and the kinds keep coming, so use a protocol, which is Chapter 10.

When both keep moving, neither answer is comfortable, and that discomfort is real rather than a failure to pick correctly.