Skip to content
academia.sh

Lesson 09 / 24

Optionals

Measuring how absence is expressed in the type, why a transforming closure never runs on nil, and where absence stops in nested layers.

Published Updated

In this lesson

  • Shows that an optional is an enumeration and is not the same type as the plain value
  • Measures that the transforming closure is never evaluated when map is called on nil
  • Reads the layer difference between map and flatMap from the output
  • Measures that a filled outer layer makes the transformer run in nested layers
Contents

The previous lesson measured closures and capture lists. A closure’s body runs where it is called, not where it is written, and a closure that is never called never has its body evaluated. This lesson asks the same question through a method that takes a closure as a parameter and sometimes never calls it. That method belongs to the most heavily used part of the type system: the type that carries absence as a value.

Most languages express absence with a special value, such as a null pointer or a magic number, and leave it to the programmer to check for that value everywhere. Swift writes absence into the type itself. An optional is a box that either holds a value or holds nothing, and it is written Int?. An Int is always an integer; an Int? may or may not be one. The compiler does not let the two types stand in for each other, so the check for absence cannot be forgotten by accident; skipping it takes a force unwrap, written on purpose.

The course’s measuring axis does not change in this lesson either: when is this expression evaluated? There are four moments: immediately, on first use, on every use, never. The Probe class, which counts by the rule, is given below exactly as it is. This lesson concentrates on the fourth moment: an expression that is never evaluated.

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)")
    }
}

Absence as a Type

Before looking at what is inside an optional, we look at the optional itself first. Int("42") converts a string to an integer, and because the conversion can fail, the return type is Int?. If it succeeds the box is filled; in a call like Int("x") the box is empty and its value is nil.

// main.swift — an optional is an enumeration
func describe(_ value: Int?) -> String {
    switch value {
    case .some(let number):
        return "filled \(number)"
    case .none:
        return "empty"
    }
}

let parsed = Int("42")
let broken = Int("x")
print(type(of: parsed))
print(describe(parsed))
print(describe(broken))
print(parsed == .some(42), broken == .none)
print(broken == nil)
Optional<Int>
filled 42
empty
true true
true

The first line gives the name of the type: Int? is only shorthand, and the real type is Optional<Int>. Optional is an enumeration defined in the standard library, and it has two cases: .some, which carries a value, and .none, which carries nothing. That is why the describe function can open the box with a switch; each case gets its own branch, and in the .some branch the carried value is bound to the name number. The fourth line shows that the value 42 equals .some(42) and that the empty box equals .none; the fifth line shows that writing nil is shorthand for .none.

A second conclusion follows from the same output: Int? and Int are two separate types. The fragments below do not compile; each was first run in a temporary measured block, and the compiler rejected both. They appear here for explanation only.

let a: Int? = 5
let b = a + 1   // does not compile: Int? and Int cannot be added
let c: Int = nil   // does not compile: nil can only be assigned to an optional type

The addition on the first line sees the box itself, not the number inside it, and the compiler asks for the box to be opened. The assignment on the second line tries to write absence into an Int. The only place that can carry absence is one whose type is marked with ?. How the box is opened is the subject of the next lesson; in this lesson we will measure two methods that work with the box without opening it.

Where Absence Comes From

Optionals do not enter a program only through string conversion. The standard library returns an optional in many places where absence is ordinary, though not everywhere: an array index out of bounds does not produce nil, it stops the program. A key that is not in a dictionary has no value; an empty array has no first element. In these cases there is no need to expect a special error value or an exception: the return type already says “maybe”.

// main.swift — the standard library reports absence with an optional
let ages = ["ada": 36, "alan": 41]
let names: [String] = []
print(ages["ada"], ages["grace"])
print(names.first, [3, 1, 2].first)
print(ages["ada"] == 36, ages["grace"] == nil)
Optional(36) nil
nil Optional(3)
true true

On the first line, the value of the existing key arrives as Optional(36); in other words, even 36 is not a plain Int but a box with 36 inside. The missing key gives nil. The second line shows the same pattern for arrays: the first property of the empty array is nil, while the first property of the non-empty array returns a boxed 3. On the third line, the box is compared with an unboxed number: the == operator automatically boxes the 36 on the right so that both sides are Int?. That is why ages["ada"] == 36 compiles, while the + example above does not, because it needs the value inside the box.

These sources make one distinction visible: the reason for the absence is not written in the optional’s type. ages["grace"] is empty because the key does not exist, and Int("x") is empty because the string is not a number, but both are the same nil. When the reason is needed, error handling comes into play; that topic arrives later in this course. When the reason does not matter, an optional is the tool with the least overhead.

map on nil

Since Optional is a two-case box, a method that transforms the value inside the box behaves differently in the two cases. map takes a transforming closure. If the box is filled, the closure is called with the value inside and the result is put in a new box; if the box is empty, the closure is not called and the result is an empty box. We measure it with two boxes, one filled and one empty, each used twice, and the body of the transforming closure marked with eval.

// main.swift — map on nil does not call the transformer
let filled = Probe("map-filled")
let vacant = Probe("map-empty")
let present: Int? = Int("21")
let missing: Int? = Int("twenty one")

for _ in 1...2 {
    _ = filled.use { present.map { filled.eval($0 * 2) } }
    _ = vacant.use { missing.map { vacant.eval($0 * 2) } }
}
filled.report()
vacant.report()
map-filled: 2 evaluations, 2 uses -> on-every-use
map-empty: 0 evaluations, 2 uses -> never

On the filled box, map runs the transformer again every time it is called: two uses, two evaluations, the moment is on every use. Notice that the result is not stored anywhere; the expression inside use runs from scratch each time. On the empty box there are two uses but no evaluations, because map returns as soon as it sees the box is empty, without ever reaching the transformer. The expression filled.eval($0 * 2) or vacant.eval($0 * 2) written in the body stays in the source, but on the empty box it is never evaluated. The rule names this case never.

This measurement shows once more when a closure’s body runs: a closure does not run where it is written, and the method that is expected to call it does not always call it. An error, a side effect or an expensive computation inside a transformer that is never called never happens. Code that relies on the transformer’s side effect misses that side effect on an empty box.

TS1. This measurement is for map only; the behavior of other methods such as filter, compactMap or forEach on an empty box was not tried. Cases where the transformer is written as a pure computation rather than with a side effect were not measured separately either.

Two maps in a Chain

If an empty box comes out of one map call empty, a second map added after it meets the same box. When the first link of the chain produces nothing, the later links find no value to work on. We measure this with two transformers; the body of each transformer is tied to its own Probe, and the chain is built from scratch on every use.

// main.swift — on an empty box every map in the chain is skipped
let stageOne = Probe("chain-first")
let stageTwo = Probe("chain-second")
let seed: Int? = Int("ten")

for _ in 1...2 {
    _ = stageOne.use { seed.map { stageOne.eval($0 + 1) }.map { stageTwo.eval($0 * 2) } }
    _ = stageTwo.use { seed.map { stageOne.eval($0 + 1) }.map { stageTwo.eval($0 * 2) } }
}
stageOne.report()
stageTwo.report()
chain-first: 0 evaluations, 2 uses -> never
chain-second: 0 evaluations, 2 uses -> never

Neither transformer was ever evaluated. The second not running might have been expected, since its own input is the result of the first; the first not running comes from the box being empty in the first place. The seed value is built with Int("ten"), so it is nil; the first map passes the empty box through without transforming it, and the second map receives the same empty box. That is why, in a long chain of transformers, an absence at the very start runs none of the later links.

TS2. In this chain only the starting value is empty; the case where a transformer in the middle of the chain returns nil is not in this measurement, and the behavior of the following map there was not measured separately.

map and flatMap

The transformer itself can also return an optional. Int($0), which converts a string to an integer, is exactly such an expression: its result is Int?. map puts the transformer’s result as it is into a new box, so a box goes inside a box. flatMap flattens the box the transformer returns out to the outside. The difference shows with three inputs: a valid string, an invalid string and an empty box.

// main.swift — map produces nested layers, flatMap flattens
let inputs: [String?] = ["7", "x", nil]
for input in inputs {
    let nested = input.map { Int($0) }
    let flat = input.flatMap { Int($0) }
    print(nested, flat)
}
Optional(Optional(7)) Optional(7)
Optional(nil) nil
nil nil

On the first line map gives a two-layer result and flatMap a single-layer one. The second line shows the real difference: "x" is an invalid string and the map result is Optional(nil), meaning the outer box is filled and the inner box is empty; flatMap gives nil directly. On the third line the input itself is empty, so both methods never reach the transformer and nil comes back.

The second line is the ground for the next measurement. Optional(nil) and nil are different values, and both express absence; but one carries the absence one layer inside, the other at the very outside. Which layer it stops at decides whether the next method will call its transformer.

TS3. This output is for the Int conversion only; cases where the transformer converts to another type or throws an error were not tried. The Optional(...) notation in the output is the print format of this version; what is measured is the number of layers, not the format itself.

The Bounding Measurement

At the start of the lesson we set a rule for map: on an empty box the transformer does not run. The rule is right, but which box counts as empty is a question of layers. The type Int?? is a two-layer box. While the outer box is filled, the box inside can be empty; this case is written .some(nil). Below are two values: one whose outer layer is filled but whose inside is empty, and one that is empty at the very outside. map is applied to both, and the transformer returns whether the value it received is nil.

// main.swift — if the outer layer is filled, map runs even when the absence is inside
let outer = Probe("outer-filled")
let hollow = Probe("outer-empty")
let inner: Int?? = .some(nil)
let empty: Int?? = nil

for _ in 1...2 {
    _ = outer.use { inner.map { value in outer.eval(value == nil) } }
    _ = hollow.use { empty.map { value in hollow.eval(value == nil) } }
}
outer.report()
hollow.report()
print(inner.map { $0 == nil })
print(empty.map { $0 == nil })
outer-filled: 2 evaluations, 2 uses -> on-every-use
outer-empty: 0 evaluations, 2 uses -> never
Optional(true)
nil

On the value whose outer layer is filled, the transformer ran and received nil: the result is Optional(true), because the transformer returned the inner absence as true. On the value that is empty at the very outside, the transformer never ran. So for map, absence is only the outermost box being empty; a value that carries absence inside but is filled outside is, as far as map is concerned, filled.

This result narrows the area in which the lesson’s rule holds. A transforming closure never being evaluated depends on the outer layer of the box being empty. When working with nested optionals, map does not carry absence “one layer up”: it hands the absence in the inner layer to the transformer as a value. That is why the Optional(nil) result in the previous section is a filled box for a later map call. When the number of layers is to be reduced, flatMap is used.

TS4. This measurement is for a two-layer box; boxes with three or more layers were not tried. We observed that the transformer received nil through the value == nil comparison; no other way of observing it was tried.

Summary

  • Int? and Int are separate types; absence is carried inside the type, and the compiler makes the check mandatory.
  • Optional is an enumeration: .some carries a value, .none (that is, nil) carries no value.
  • On an empty box map never calls the transforming closure; on a filled box it runs it again on every call.
  • When the transformer returns an optional, map adds a layer and flatMap flattens; Optional(nil) and nil are different values.
  • In an Int?? whose outer layer is filled, map runs the transformer even though absence is inside.
  • This lesson’s moment: never. On an empty box, the map transformer is never evaluated.

Next Step

In this lesson we worked without opening the box: map and flatMap transformed the value but left the result in a box again. When a real value is needed, the box has to be opened. The next lesson measures the ways of opening it: when if let and guard let evaluate their right-hand side, at which candidate a ?? chain stops, and what optional chaining does to the rest of the chain when it sees nil.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close