Home

Enums and pattern matching

Making illegal states unrepresentable

Swift’s enum is not the C enum. It is a tagged union: each case may carry its own payload, of its own type, and the compiler guarantees exactly one case holds at a time.

That guarantee is the whole value. A type that can be in exactly one of four states, each carrying exactly the data that state needs, cannot be put into a fifth state by any code you or anyone else writes.

enum LoadState {
    case idle
    case loading(progress: Double)
    case loaded(items: [Item])
    case failed(Error)
}

Compare with the shape this replaces:

struct LoadState {
    var isLoading: Bool
    var progress: Double?
    var items: [Item]?
    var error: Error?
}

That struct has sixteen combinations of nil-ness, of which four are meaningful. isLoading == true with a non-nil error is representable, so somewhere there is a branch handling it, or — more likely — there is not, and it happens once a month in production.

Exhaustiveness is the feature

A switch over an enum must handle every case, and that check is what makes enums safe to change. Add a case, and the compiler lists every place that needs updating.

switch state {
case .idle:
    return AnyView(StartButton())
case .loading(let progress):
    return AnyView(ProgressView(value: progress))
case .loaded(let items):
    return AnyView(ItemList(items: items))
case .failed(let error):
    return AnyView(ErrorView(error: error))
}

Warning

A default: clause throws that away. It silences the compiler for cases that do not exist yet, which is exactly the warning you wanted. Use default only for genuinely uninteresting remainders, and prefer listing cases with a shared body: case .idle, .failed: …

The exception is an enum from another module, which may gain cases without your code being recompiled. Those require @unknown default: — a clause that still warns when you compile against a newer version, so you keep the notification without breaking the build.

Associated values and pattern matching

Patterns go deeper than one level, which is where switch stops being a glorified if:

switch (oldState, newState) {
case (.loading, .loaded(let items)) where items.isEmpty:
    showEmptyState()
case (.loading, .loaded(let items)):
    show(items)
case (_, .failed(let error as URLError)) where error.code == .notConnectedToInternet:
    showOfflineBanner()
case (_, .failed(let error)):
    show(error)
default:
    break
}

Tuples, nested patterns, where clauses and type casts all compose in the same expression. A state machine written this way reads as a table of transitions, which is what it is.

For matching a single case, if case and guard case avoid a whole switch:

guard case .loaded(let items) = state else { return }

Recursive enums

An enum case cannot normally contain a value of its own type — the size would be infinite. indirect adds a level of indirection so it can:

indirect enum Expression {
    case number(Double)
    case add(Expression, Expression)
    case multiply(Expression, Expression)
    case negate(Expression)
}

func evaluate(_ expression: Expression) -> Double {
    switch expression {
    case .number(let value):           value
    case .add(let a, let b):           evaluate(a) + evaluate(b)
    case .multiply(let a, let b):      evaluate(a) * evaluate(b)
    case .negate(let inner):          -evaluate(inner)
    }
}

That is a complete expression tree and evaluator in fifteen lines, with the compiler guaranteeing the evaluator handles every node type. indirect boxes the payload on the heap, so it costs an allocation per node — worth knowing, rarely worth avoiding.

What the compiler does with them

Enum layout is more clever than it looks, and it explains why Optional is free.

An enum with no associated values is stored as a single byte. An enum whose cases carry payloads is the size of its largest payload plus, usually, a tag byte.

The interesting case is when a payload has spare bit patterns. A reference is never null in Swift, so the all-zeroes pattern is unused — and Optional<SomeClass> uses it for .none. The result is that an optional class reference is the same eight bytes as the reference, with no tag at all.

MemoryLayout<UnsafeRawPointer>.size          // 8
MemoryLayout<UnsafeRawPointer?>.size         // 8 — free
MemoryLayout<Bool>.size                      // 1
MemoryLayout<Bool?>.size                     // 2 — Bool uses only two of 256 patterns,
                                             //     but the layout still adds a byte here

This is worth knowing mainly to stop optimising it away. Making a property non-optional to “save memory” usually saves nothing and costs you the ability to represent absence.

Where a struct is the better answer

Enums are for one of several, structs for all of these together. The mistake is reaching for an enum when cases share most of their data:

// wrong: every case carries the same three fields
enum User {
    case admin(name: String, email: String, since: Date)
    case member(name: String, email: String, since: Date)
}

// right
struct User {
    let name: String
    let email: String
    let since: Date
    let role: Role       // enum { case admin, member }
}

The test: if two cases carry the same payload, the difference between them is a field, not a case.