Skip to content
academia.sh

Lesson 01 / 24

Swift's Design Goals

Ties Swift's three goals — safety, expressiveness, performance — to one measurement axis: when is this expression evaluated? Splits eight expressions into four moments.

Published Updated

In this lesson

  • Relate Swift's safety, expressiveness and performance goals to concrete behavior
  • Classify the moment an expression is evaluated as immediately, on first use, on every use or never
  • Explain the rule and the limit of the `Probe` class in the measurement harness
Contents

In earlier courses you asked what a program does. This course asks something else: when you write an expression, at which moment of the program does it run? You saw the split between compile time and run time in an earlier course (M11/K01); here we measure the order inside the run. Swift is the language of the iOS curriculum, and it is customary to introduce it through three goals.

Three Goals

Safety: the language tries to catch errors before the run. A constant cannot be read before it is assigned, and a value cannot be nil unless its type is an optional. In both cases the compiler rejects the program. These snippets do not compile in Swift 6.4; the compiler rejects both, which is why they have no output.

let total: Int
print(total)
var name: String = nil

Expressiveness: you do not have to write the type, because the compiler infers it from the right-hand side (type inference). The performance goal is not measured in this lesson; we do not measure time, only order.

The Measurement Axis: Four Moments

Throughout the course we ask a single question: when is this expression evaluated? There are four answers: immediately, on first use, on every use and never. The measurement harness has two parts. First comes the helper file; it stays the same through the whole lesson and always stands here as the first Swift block.

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

eval wraps the expression and counts that it was evaluated; use wraps the access to the expression. The rule is simple: if it was never evaluated, never; if it was evaluated more than once, on-every-use; if it was evaluated once and that happened before the first use, immediately; otherwise on-first-use.

LB1. Every observation except never carries at least two uses; with a single use, one evaluation cannot tell immediately and on-first-use apart.

Eight expressions, eight Probe objects. A constant, a lazy property, a computed property, a function, a short circuit and a closure sit side by side in the same file; the course will split them into separate lessons one by one.

// main.swift — eight expressions, four moments
final class Basket {
    let lazyProbe: Probe
    let computedProbe: Probe
    var items = [3, 4, 5]

    init(lazyProbe: Probe, computedProbe: Probe) {
        self.lazyProbe = lazyProbe
        self.computedProbe = computedProbe
    }

    lazy var total: Int = lazyProbe.eval(items.reduce(0, +))
    var count: Int { computedProbe.eval(items.count) }
}

func square(_ x: Int, probe: Probe) -> Int { probe.eval(x * x) }

let constant = Probe("let-constant")
let lazyTotal = Probe("lazy-property")
let computed = Probe("computed-property")
let fallback = Probe("nil-coalescing-rhs")
let call = Probe("function-body")
let shortCircuit = Probe("and-operator-rhs")
let captured = Probe("capture-list")
let body = Probe("closure-body")

let limit = constant.eval(10)
_ = constant.use { limit }
_ = constant.use { limit }

let basket = Basket(lazyProbe: lazyTotal, computedProbe: computed)
_ = lazyTotal.use { basket.total }
_ = lazyTotal.use { basket.total }
_ = computed.use { basket.count }
_ = computed.use { basket.count }

let stored: Int? = 7
_ = fallback.use { stored ?? fallback.eval(0) }
_ = fallback.use { stored ?? fallback.eval(0) }

_ = call.use { square(3, probe: call) }
_ = call.use { square(4, probe: call) }

let ready = false
_ = shortCircuit.use { ready && shortCircuit.eval(true) }
_ = shortCircuit.use { ready && shortCircuit.eval(true) }

let step = 2
let addStep = { [step = captured.eval(step)] (x: Int) -> Int in x + step }
_ = captured.use { addStep(1) }
_ = captured.use { addStep(2) }

let double = { (x: Int) -> Int in body.eval(x * 2) }
_ = body.use { double(3) }
_ = body.use { double(4) }

let probes = [constant, lazyTotal, computed, fallback, call, shortCircuit, captured, body]
for probe in probes { probe.report() }
tally(probes)
let-constant: 1 evaluations, 2 uses -> immediately
lazy-property: 1 evaluations, 2 uses -> on-first-use
computed-property: 2 evaluations, 2 uses -> on-every-use
nil-coalescing-rhs: 0 evaluations, 2 uses -> never
function-body: 2 evaluations, 2 uses -> on-every-use
and-operator-rhs: 0 evaluations, 2 uses -> never
capture-list: 1 evaluations, 2 uses -> immediately
closure-body: 2 evaluations, 2 uses -> on-every-use
immediately: 2
on-first-use: 1
on-every-use: 3
never: 2

The eight expressions split into four moments: two immediately, one on-first-use, three on-every-use, two never. This count is the course’s binding tally; the later lessons open each expression in its own lesson. A let constant is evaluated on the line where it is defined. A lazy property is built when it is first read and then stored, while a computed property is calculated again on every read. A function body runs on every call; the right side of the ?? operator never runs because the left side is filled, and the right side of the && operator never runs because the left side is false. A capture list is evaluated once, when the closure is created; the closure’s body is evaluated on every call.

LB2. The constant and the capture list land on the same moment, but by different paths; this lesson measures only the moment, not the mechanism.

A Value Copy Does Not Recompute

One leg of Swift’s safety goal is value semantics: value types such as arrays, strings and structures are copied on assignment, and the copy is an independent value. When you assign one variable to another, changing either one does not affect the other. This property comes from the design concern that no state should be shared by accident. The measurement question is a different one: does the copy re-evaluate the expression that produced the source, or does it merely duplicate the existing value?

The block below produces an array with a Probe, copies it into a second variable named duplicate, and changes the two arrays independently. It then reads both variables; the reads count as two uses.

// Copy.swift — a copy does not run the expression again
let sourceProbe = Probe("source-array")
var original = sourceProbe.eval([1, 2, 3])
var duplicate = original
duplicate.append(4)
original.append(9)
_ = sourceProbe.use { original.count }
_ = sourceProbe.use { duplicate.count }
print(original, duplicate)
sourceProbe.report()
[1, 2, 3, 9] [1, 2, 3, 4]
source-array: 1 evaluations, 2 uses -> immediately

The 9 appended to original did not show up in duplicate, and the 4 appended to duplicate is not in original. The two variables separated at the moment of assignment and each went its own way. The measurement line says the expression was evaluated only once: assignment duplicates a value, it does not run the expression that produced it again. Because the evaluation happens in the var definition, before the two reads, the moment comes out as immediately.

This distinction makes the safety goal concrete. If assignment recomputed the source, every copy could produce its result at a different moment; in the measured behavior, a copy is a snapshot of the value at the moment of assignment. Later changes apply to each side separately.

LB3. The independence of the copy was measured with an array of Int; sharing of elements in the copy of an array that holds class instances is not the subject of this lesson.

A Constant Binding Does Not Freeze the Object

Another end of the safety goal concerns let, and it is often misread. When you bind a class instance with let, what is bound is the reference to the instance. Writing a constant instead of a variable stops the name holder from pointing at another object, but it does not lock the object’s own var properties. You can read the same rule the other way round for value types: if you bind a structure with let, you cannot change its properties either; a class has no such ban.

The block below defines a small class, binds it with let, and still changes the counter inside it with two assignments. The counter value comes through a Probe; two separate assignments bring two separate evaluations.

// Holder.swift — let does not freeze the object
final class Holder {
    var count = 0
}
let holderProbe = Probe("class-counter")
let anchor = Holder()
_ = holderProbe.use { anchor.count = holderProbe.eval(5) }
_ = holderProbe.use { anchor.count = holderProbe.eval(8) }
print(anchor.count)
holderProbe.report()
8
class-counter: 2 evaluations, 2 uses -> on-every-use

Although anchor is a let constant, anchor.count was assigned twice and the final value, 8, was read. What is constant is the reference that the name anchor is bound to; the inside of the object is variable. The measurement line shows two evaluations, because the two assignments made two separate eval calls: the let rule does not restrict the number of assignments here, since what is assigned is not anchor but the anchor.count field. That is why the moment is on-every-use.

The design takeaway is this: let guarantees that a binding cannot be made again; it does not guarantee that the object is immutable. If you want immutability, make the fields let as well, or use a value type.

LB4. The class instance was measured with a single field; the claim that the same assignment does not compile on a value type bound with let lies outside this lesson, as source-level knowledge.

Type Inference Does Not Change the Moment

The most visible part of the expressiveness goal is type inference: if you write let size = 12, the compiler finds the Int type itself and you do not have to write it. If you like, you can also write the type explicitly: let size: Int = 12. The question that comes to mind is whether inference has a run-time cost or a timing. Type inference finishes at compile time; the running program does not know how the type was found.

The block below defines the same value in two ways: one by inference, one with an explicit type annotation. Each is tied to its own Probe and read twice; the types of both variables are printed with type(of:).

// Inference.swift — inference and an explicit type give the same moment
let inferredProbe = Probe("inferred-type")
let declaredProbe = Probe("explicit-type")
let inferred = inferredProbe.eval(42)
let declared: Int = declaredProbe.eval(42)
_ = inferredProbe.use { inferred + 1 }
_ = inferredProbe.use { inferred + 2 }
_ = declaredProbe.use { declared + 1 }
_ = declaredProbe.use { declared + 2 }
print(type(of: inferred), type(of: declared))
inferredProbe.report()
declaredProbe.report()
Int Int
inferred-type: 1 evaluations, 2 uses -> immediately
explicit-type: 1 evaluations, 2 uses -> immediately

The two variables print the same type, and the two Probe objects gave the same moment: both are one evaluation, two uses, immediately. The explicit type annotation did not move the expression to another time; it only told the compiler what to find. This is why the course’s measurement axis is insensitive to type inference: what determines when an expression is evaluated is not whether the type is written, but the context in which the expression stands.

LB5. Type inference was measured in this lesson only with an Int literal; the claim that inference leaves timing untouched for other literals and kinds of expression lies outside this lesson.

A Lazy Transformation Waits Until It Is Called

The expressiveness goal of the design also raises the question of when something written briefly actually runs. When you call map on an array, the transformation is done immediately, for every element. If you call map on the lazy view of the same array, the transformation runs for no element at the moment it is defined; it runs only when an element is requested, and only for that element. This is not the array counterpart of the lazy var property we saw earlier, but it carries the same intuition: work can be deferred until someone asks for the result.

The block below sets up two Probe objects. The body of the eager transformation records evaluations on eagerProbe, and the body of the lazy transformation records them on lazyProbe. The counters are printed right after the transformations and again after two elements are pulled from the lazy view. The evaluations property is a readable counter inside Probe.

// LazyMap.swift — eager and lazy transformation
let eagerProbe = Probe("eager-transform")
let lazyMapProbe = Probe("lazy-transform")
let values = [1, 2, 3]
let eagerDoubled = values.map { eagerProbe.eval($0 * 2) }
let lazyDoubled = values.lazy.map { lazyMapProbe.eval($0 * 2) }
print(eagerProbe.evaluations, lazyMapProbe.evaluations)
_ = lazyMapProbe.use { Array(lazyDoubled.prefix(2)) }
print(eagerProbe.evaluations, lazyMapProbe.evaluations)
_ = lazyMapProbe.use { Array(lazyDoubled.prefix(2)) }
print(eagerDoubled, Array(lazyDoubled.prefix(1)))
lazyMapProbe.report()
3 0
3 2
[2, 4, 6] [2]
lazy-transform: 5 evaluations, 2 uses -> on-every-use

The first line came out as 3 0: the eager map transformed all three elements immediately, while the lazy map had not yet touched any. On the second line the lazy counter became 2, because prefix(2) asked for the transformation of only two elements; the third element was never computed. When the same prefix(2) was called a second time, the transformation ran again, because a lazy view does not store its results; the five evaluations on the last report line show this: two, two, and one from the final prefix(1) call.

The expressiveness goal leaves it to you to tell these two behaviors apart. The eager map produces the result once and stores it. The lazy view keeps the writing short, but it may redo the work on every pull. Which is right depends on how many times the result will be read and whether the body has a side effect.

LB6. The lazy transformation was measured only for elements pulled with prefix; how the lazy view changes the count under filter or chained transformations lies outside this lesson.

The Bounding Measurement

The rule counts, it does not read intent. If you write the same let expression as two separate constants with two separate Probe objects, both come out immediately. If you tie the same expression to a single Probe, the second eval raises the count to two and the rule says on-every-use; the intent of the code has not changed, but the measurement harness has.

// Twin.swift — the same expression, two probes and one probe
let first = Probe("first-constant")
let second = Probe("second-constant")
let shared = Probe("shared-constant")

let a = first.eval(5)
let b = second.eval(5)
let c = shared.eval(5)
let d = shared.eval(5)

for probe in [first, second, shared] {
    _ = probe.use { a + b + c + d }
    _ = probe.use { a + b + c + d }
    probe.report()
}
first-constant: 1 evaluations, 2 uses -> immediately
second-constant: 1 evaluations, 2 uses -> immediately
shared-constant: 2 evaluations, 2 uses -> on-every-use

LB7. This measurement reads the number of eval calls, not how the expression is written; we do not count Probe as a language feature.

Summary

  • Swift provides safety through the compiler: an unassigned constant cannot be read, and a non-optional value cannot be nil.
  • Expressiveness is type inference; the performance goal is not measured in this lesson.
  • The course’s measurement axis: when is this expression evaluated? Four moments: immediately, on first use, on every use, never.
  • The Probe rule reads the count; it does not read intent.
  • This lesson’s moment: immediately (a let constant, on the line where it is defined).

Next Step

A constant is evaluated the moment it is defined; but what happens when a var is reassigned, and can a constant be assigned after it has been defined? The next lesson measures the assignment moments of constants and variables.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close