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.