Lesson 12 / 24
Enumerations
Measuring when a raw value and an associated value are set up, how pattern matching opens them, and why a computed property runs on every read.
Published Updated
In this lesson
- Measures the rawValue and init(rawValue:) paths of an enumeration with raw values
- Shows that an associated value is evaluated once, when the case is constructed
- Reads from the output that pattern matching with switch binds the associated value to a name
- Measures that a computed property in an enumeration runs again on every read
Contents
The previous lesson measured types that are lists of fields: structures and classes always carry the same fields. Some data, however, calls not for a list of fields but for being in one of several distinct states. A day of the week is one of seven values; a measurement is either a number in degrees or a label. These two examples describe different things, but both are expressed with an enumeration.
A Swift enumeration is more powerful than its counterpart in many languages. Each case is not only a name; depending on its definition it can have a raw value, or an associated value that travels with the case. Enumerations can also carry computed properties and methods; they cannot carry stored instance properties. The lesson first measures when these two kinds of value are set up, and then when a computation inside an enumeration runs. The course’s measuring axis does not change: when is this expression evaluated? There are four moments: immediately, on first use, on every use, never. The Probe class is given below exactly as it is.
The Core
Every lesson uses the same core. Probe counts how many times an expression is evaluated (eval) and how many times it is used (use); the moment property derives the moment from those two numbers. In the binding measurement of the opening lesson, the distribution over eight expressions was 2/1/3/2: two immediately, one on first use, three on every use, two never. In this lesson we measure only our own slice.
// Probe.swift — records when an expression is evaluated
final class Probe {
let label: String
private(set) var evaluations = 0
private(set) var uses = 0
private var evaluatedBeforeUse = false
init(_ label: String) {
self.label = label
}
func eval<T>(_ value: T) -> T {
evaluations += 1
if uses == 0 { evaluatedBeforeUse = true }
return value
}
func use<T>(_ access: () -> T) -> T {
uses += 1
return access()
}
var moment: String {
if evaluations == 0 { return "never" }
if evaluations > 1 { return "on-every-use" }
return evaluatedBeforeUse ? "immediately" : "on-first-use"
}
func report() {
print("\(label): \(evaluations) evaluations, \(uses) uses -> \(moment)")
}
}
func tally(_ probes: [Probe]) {
for moment in ["immediately", "on-first-use", "on-every-use", "never"] {
let count = probes.filter { $0.moment == moment }.count
print("\(moment): \(count)")
}
}
The measurements use two enumerations. Weekday has an Int raw value and lists all its cases through CaseIterable; when the first case is given 1, the following ones become 2 and 3. Reading carries a value with its cases: celsius takes a Double, label takes a String. This block has no output, because it only defines the types.
// Types.swift — enumerations with raw values and with associated values
enum Weekday: Int, CaseIterable {
case monday = 1, tuesday, wednesday
}
enum Reading {
case celsius(Double)
case label(String)
}
Raw Values
A raw value is the counterpart assigned to a case as a constant at compile time. The raw value of each case must be different, and the values are written in the definition. When 1 is written for the first case, the values of the following cases are determined by adding one each time. The raw value is reached through the rawValue property, and to go the other way, from a raw value to a case, the form Weekday(rawValue: 3) is used. It is clear that the reverse direction cannot find a case for every raw value; this form therefore returns an optional.
// main.swift — raw value, construction with rawValue, and allCases print(Weekday.tuesday.rawValue) print(Weekday(rawValue: 3) == .wednesday) print(Weekday(rawValue: 9) == nil) print(Weekday.allCases.count)
2 true true 3
The first line gives the raw value of the tuesday case: because the first case started at 1, it is 2. The second line shows that the value constructed with rawValue: 3 equals .wednesday. The third line shows that for 9, which is the raw value of no case, the result is nil; the empty box we saw in the first lesson is here the result of searching for a case, not of an Int conversion. The fourth line shows that allCases, added by CaseIterable, counts three cases.
Here the raw value is a tool for identity and conversion. It is used to convert an integer coming from a network response, a file or a database field safely into a case, and it forces the caller to decide what to do with an invalid value.
A raw value does not have to be an integer. In an enumeration whose type is String, the raw value of each case is the case’s own name, unless written explicitly. We do the same three operations with a list of planets: reading the raw value, constructing a case from a raw value, and trying to construct from an invalid raw value.
// main.swift — a String raw value is the case name unless specified
enum Planet: String {
case mercury, venus, earth
}
print(Planet.venus.rawValue)
print(Planet(rawValue: "earth") == .earth)
print(Planet(rawValue: "pluto") == nil)
venus true true
The first line shows that the raw value of the venus case is "venus" although no value was written. The second line shows that the raw value "earth" gives the .earth case, and the third line shows that for "pluto", which is not in the list, the result is nil. As in the integer example, the reverse direction returned an optional here too: the path from a raw value to a case always carries the possibility of absence.
TS14. These measurements are for one enumeration with an Int raw value and one with a String raw value. Other raw value types (Double, Character) and the behavior of Int cases with raw values written for only some of them were not measured separately.
Associated Values and Pattern Matching
An associated value works differently from a raw value. A raw value is fixed at definition and is the same in every instance of the same case. An associated value is determined when the case is constructed, and each instance carries its own value. Reading.celsius(21.5) and Reading.celsius(30.0) are the same case but carry different data. To reach what is inside such a value, pattern matching with switch is used: in the branch of each case, the associated value is bound to a name.
// main.swift — opening an associated value with pattern matching
let readings = [Reading.celsius(21.5), Reading.label("hot")]
for reading in readings {
switch reading {
case .celsius(let degrees):
print("degrees", degrees)
case .label(let text):
print("label", text)
}
}
degrees 21.5 label hot
The two Reading values in the array enter the switch in turn. The first selects the celsius branch and binds the 21.5 inside it to the name degrees; the second selects the label branch and binds the string "hot" to the name text. In each branch the bound name is valid only in the body of that branch, and its type is the type of that case’s associated value. The compiler checks that all cases are handled: if a case is skipped, the switch is considered incomplete, and a branch for the missing case or a default has to be written.
This behavior is the same pattern as the optional of the first lesson. Optional is also an enumeration, and its .some case carries an associated value; if let and case .some(let value) inside a switch are two spellings of the same matching. This lesson gives a second example of why the box in that lesson behaves the way it does.
When only a single case has to be looked at, writing a switch is heavy. if case writes the same matching as a single condition: if the case matches, the associated value is bound and the block runs; if it does not match, the else branch runs. One more condition can be added with a comma; after the associated value is bound, it is used in that condition.
// main.swift — looking at a single case with if case
let current = Reading.celsius(18.0)
if case .celsius(let degrees) = current, degrees < 20 {
print("cool", degrees)
}
if case .label(let text) = current {
print("label", text)
} else {
print("not a label")
}
cool 18.0 not a label
The first if case matched current with the celsius case, bound 18.0 to the name degrees, and also passed the degrees < 20 condition after the comma. The second if case tried to match the same value with the label case, and because there was no match it ran the else branch. This is the same behavior as switch: when a case does not match, the associated value is never bound and the related name is not in scope.
TS15. This measurement is for an enumeration with two cases, each with a single associated value. Cases with several associated values, indirect enumerations and branches with a where condition were not measured.
When the Associated Value Is Set Up
An associated value is an expression given when the case is constructed. When Reading.celsius(stamp.eval(19.0)) is written, the expression stamp.eval(19.0) is evaluated once while the case is constructed, and the result is stored inside the case. When it is later read with switch, the expression is not computed again; what is read is the value stored in the case. We measure this by reading twice.
// main.swift — the associated value is evaluated once, when the case is constructed
let stamp = Probe("associated-value")
let reading = Reading.celsius(stamp.eval(19.0))
for _ in 1...2 {
_ = stamp.use { () -> Double in
switch reading {
case .celsius(let degrees):
return degrees
case .label:
return -1
}
}
}
stamp.report()
associated-value: 1 evaluations, 2 uses -> immediately
One evaluation, two uses, moment immediately. The evaluation took place on the line where reading was constructed, before the use calls. Both switch statements read the same stored value and did not run the expression again. This differs from the raw value: the expression of a raw value never runs, because it is a constant. The expression of an associated value runs once per instance.
The practical result the measurement shows is this: an expensive computation written into an associated value is paid when the case is constructed, and reads need no extra computation. Code that wants the computation done at the moment of reading should use a computed property, not an associated value. The next section measures that property.
TS16. This measurement is for an expression that wraps a constant Double literal. Cases where the associated value is a function call, or an expression containing another associated value, were not measured separately.
The Bounding Measurement
A raw value and an associated value are fixed when the case is constructed or in the definition; that is, reading them costs nothing. An enumeration, however, can carry a computed property as well as stored values. This property stores no value; it runs its body every time it is read. In the Sensor enumeration, the fahrenheit property converts the Celsius degrees, carried as an associated value, to Fahrenheit on every read. The conversion expression is marked with a Probe, and the property is read twice.
// main.swift — an enumeration's computed property runs on every read
enum Sensor {
case reading(Probe, Double)
var fahrenheit: Double {
switch self {
case .reading(let probe, let celsius):
return probe.eval(celsius * 9 / 5 + 32)
}
}
}
let derived = Probe("computed-property-sensor")
let sensor = Sensor.reading(derived, 20)
_ = derived.use { sensor.fahrenheit }
_ = derived.use { sensor.fahrenheit }
derived.report()
print(sensor.fahrenheit)
computed-property-sensor: 2 evaluations, 2 uses -> on-every-use 68.0
Two reads, two evaluations, moment on every use. A computed property does not store its result; each time sensor.fahrenheit is written, its body runs from the start. The associated value 20 was fixed at construction, but the 68.0 derived from it is computed again on every read. The second line of the output shows that the next read of the same property gives the same result.
This result narrows the lesson’s rule. The sentence “an associated value is evaluated once when the case is constructed” is true only for the associated value itself. Every computed property derived from an associated value runs from the start on every read. When the same computation is needed in several places, keeping the result in a name is cheaper than reading the computed property again everywhere.
There is also a limit for raw values, and it cannot be measured, because it does not compile. A raw value must be a constant written in the source. The fragment below tries to assign the return of a function as a raw value and does not compile: the compiler rejects it. The error message said that a raw value has to be written as a constant. The fragment stands here only for explanation.
func rawOf() -> Int { 3 }
enum Bad: Int {
case a = 1
case b = rawOf() // does not compile: a raw value must be a constant
}
This limit shows a second difference between the two kinds of value. An associated value can be any expression evaluated at runtime; a raw value is a constant known at compile time. The expression of a raw value is therefore never evaluated at runtime.
TS17. This measurement is for a single computed property derived from two stored values. A computed property that is not read-only (one with a set) and the methods of the enumeration were not measured separately. In the rejected raw value fragment only a function call was tried; other forms of expression were not tried.
Reading the Three Values Together
Three different kinds of value were measured in this lesson, and each fell on a different moment. A raw value is a constant written in the definition; as an expression it never runs at runtime, so no measuring probe can be attached to it. An associated value is an expression given when the case is constructed and is evaluated once per instance; in the measurement this moment came out immediately. A computed property stores no value and runs its body on every read; in the measurement this moment came out on every use.
Although the three kinds may look interchangeable, they answer different questions. If a code coming from outside has to be converted into a case, a raw value fits, because the reverse direction returns an optional and makes invalid input visible. If instance-specific data has to be carried next to a case, an associated value fits, because different instances of a case can carry different data. If a result derived from the carried data has to be produced at the moment it is needed, a computed property fits, but the price paid on every read has to be counted too.
Summary
- A raw value is fixed in the definition, and
Weekday(rawValue:)returns an optional; a value that is not found givesnil. - Pattern matching with
switchbinds the associated value to a name in each branch and checks that all cases are handled. - An associated value is evaluated once when the case is constructed; reads do not run the expression again.
- A computed property in an enumeration stores no result and runs again on every read.
- A raw value must be a constant known at compile time; a function call cannot be a raw value.
- This lesson’s moment: immediately. The expression of an associated value is evaluated on the line where the case is constructed.
Next Step
In this lesson we saw that a property in an enumeration runs on every read. Properties do not belong to enumerations alone: structures and classes also carry stored, computed and observed properties, and there is lazy, which defers an expression to the first read. The next lesson measures these kinds of property and when each of them sets up its value.
To keep your progress and take notes, Log in
My notes
Log in to take notes.