---
title: 'Result Builders'
source: 'https://academia.sh/en/courses/swift/result-builders'
course: 'The Swift Language'
language: en
updated: '2026-10-21T05:03:12+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Writes a result builder that builds a list of strings with buildBlock and buildExpression'
    - 'Measures that expressions in an unchosen if or else branch are never evaluated'
    - 'Shows that a for loop evaluates every element on every call through buildArray'
    - 'Tells apart that the choice of branch is made at run time and not at compile time'
---

# Result Builders

Writes a result builder of its own and measures that expressions in an unchosen if or else branch are never evaluated, that a for loop evaluates every element, and that the branch is chosen at run time and not at compile time.

The previous lesson moved the value rule of a property into a separate type. A **result builder (`@resultBuilder`)** applies the same idea to a list of expressions. Expressions written one under another in the body of a closure normally run one by one and their values are discarded. In a closure passed to a parameter marked with a result builder, each line is instead given to a method of the builder type, and the values those methods return are combined into a single result. Which method each line is bound to, and how, decides at which moment the line runs.

This lesson writes a small builder that builds a list of strings and measures three things: whether an expression in an unchosen `if` branch is evaluated, how a `for` loop processes its elements, and when the choice of branch is made. The transformation itself happens at compile time and is not visible at run time; what is measured is the run-time behavior of the transformed code. This split between compile time and run time was covered separately in an earlier curriculum.

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

```swift
// ListBuilder.swift — a result builder that builds a list of strings
@resultBuilder
struct ListBuilder {
    static func buildExpression(_ line: String) -> [String] {
        [line]
    }

    static func buildBlock(_ parts: [String]...) -> [String] {
        parts.flatMap { $0 }
    }

    static func buildOptional(_ part: [String]?) -> [String] {
        part ?? []
    }

    static func buildEither(first part: [String]) -> [String] {
        part
    }

    static func buildEither(second part: [String]) -> [String] {
        part
    }

    static func buildArray(_ parts: [[String]]) -> [String] {
        parts.flatMap { $0 }
    }
}

func makeList(@ListBuilder _ content: () -> [String]) -> [String] {
    content()
}
```

## Writing the Builder

`ListBuilder` carries six static methods. `buildExpression` turns each single expression into a list of strings. `buildBlock` joins the parts of a block one after another. `buildOptional` handles the result of an `if` that has no `else` branch; if the branch is not chosen, it gives an empty list. `buildEither(first:)` and `buildEither(second:)` handle whichever of the `if` and `else` branches was chosen. `buildArray` joins the parts that come from each iteration of a `for` loop. `makeList` is a helper function that takes its closure as a parameter marked with `ListBuilder`.

## The Unchosen Branch Is Not Evaluated

The first measurement writes two expressions inside `if`/`else`. One calls a `Probe` named `chosen`, the other a `Probe` named `skipped`. The condition is always true, so only the first branch is chosen. The `render` function is called twice.

```swift
// main.swift — the expression in the unchosen branch is not evaluated
let chosen = Probe("chosen-branch")
let skipped = Probe("skipped-branch")

do {
    let useFirst = true

    func render() -> [String] {
        makeList {
            "title"
            if useFirst {
                chosen.eval("first")
            } else {
                skipped.eval("second")
            }
        }
    }

    for _ in 1...2 {
        let lines = chosen.use { skipped.use { render() } }
        print(lines.joined(separator: ","))
    }
}

chosen.report()
skipped.report()
```

```
title,first
title,first
chosen-branch: 2 evaluations, 2 uses -> on-every-use
skipped-branch: 0 evaluations, 2 uses -> never
```

In both calls the list is `title,first`. `chosen-branch` records two evaluations, because the body of the builder runs again on every `render` call. `skipped-branch` sees two uses but zero evaluations: the expression of the second branch never runs. The rule says `never`. This lesson's moment is **never**: the expression of an unchosen branch is not evaluated at any moment. Merely being written does not call the expression.

## The Loop Evaluates Every Element

A `for` loop is combined with `buildArray`, and the expression in its body is evaluated once for each element. The measurement processes an array of three elements twice and adds to the same closure a line that contains an `if`; that line goes through the `buildOptional` path.

```swift
// main.swift — for evaluates every element
let items = Probe("loop-item")

do {
    func render(names: [String]) -> [String] {
        makeList {
            for name in names {
                items.eval(name)
            }
            if names.count > 2 {
                "long list"
            }
        }
    }

    for _ in 1...2 {
        let lines = items.use { render(names: ["a", "b", "c"]) }
        print(lines.joined(separator: ","))
    }
}

items.report()
```

```
a,b,c,long list
a,b,c,long list
loop-item: 6 evaluations, 2 uses -> on-every-use
```

On every call the list is `a,b,c,long list`. `loop-item` records six evaluations: three elements in each of two calls. The elements were not cached once; the body of the builder runs again from the start on every call, and the loop processes every element again. Its moment is `on-every-use`. The string of the line `if names.count > 2` did not pass through a `Probe`; it only appeared in the list.

## Expression Conversion

`buildExpression` converts each single expression of the block into the part type of the builder. If more than one `buildExpression` is written under the same name, the compiler picks the one that matches the type of the expression. The `MixedBuilder` below accepts both `String` and `Int`; it turns numbers into a string of the form `number 42`. The measurement passes the two expressions through separate `Probe` objects and looks at how many times each expression is evaluated on every call.

```swift
// MixedBuilder.swift — a result builder that accepts text and number expressions
@resultBuilder
struct MixedBuilder {
    static func buildExpression(_ line: String) -> [String] {
        [line]
    }

    static func buildExpression(_ number: Int) -> [String] {
        ["number \(number)"]
    }

    static func buildBlock(_ parts: [String]...) -> [String] {
        parts.flatMap { $0 }
    }
}

func makeMixed(@MixedBuilder _ content: () -> [String]) -> [String] {
    content()
}
```

```swift
// main.swift — buildExpression converts the expression, it does not re-evaluate it
let texts = Probe("text-expression")
let numbers = Probe("number-expression")
do {
    for _ in 1...2 {
        let lines = texts.use {
            numbers.use {
                makeMixed {
                    texts.eval("title")
                    numbers.eval(42)
                }
            }
        }
        print(lines.joined(separator: ","))
    }
}
texts.report()
numbers.report()
```

```
title,number 42
title,number 42
text-expression: 2 evaluations, 2 uses -> on-every-use
number-expression: 2 evaluations, 2 uses -> on-every-use
```

The output is `title,number 42` in both calls: the string expression passed as it was, and the integer expression was converted into the form `number 42`. Which path did the conversion is clear from the type of the expression; no separation was written in the builder. The counters show that the conversion did not change the evaluation. `text-expression` and `number-expression` record two evaluations each across the two calls and say `on-every-use`. `buildExpression` takes the result of the expression; it does not run the expression again. The builder runs the body of the closure from the start once per call, and each expression is evaluated exactly once. So when writing a conversion there is no need to worry that an expression with side effects will be repeated. Writing more than one `buildExpression` under the same name widens the expression types the builder accepts; an expression whose type is not covered gives a compile error, and it is not silently skipped at run time.

## Nested Conditions

An `if` inside an `if` opens two nested `buildOptional` paths in the builder. If the outer condition does not hold, the inner condition itself is not evaluated either. The measurement runs the same closure twice with `visible: false`, then twice with `visible: true`. In each case there is a separate `Probe` for the inner condition and for the expression of the inner line.

```swift
// main.swift — if the outer condition does not hold, the inner condition is not evaluated
let closedGate = Probe("closed-inner-condition")
let closedHit = Probe("closed-inner-line")
let openGate = Probe("open-inner-condition")
let openHit = Probe("open-inner-line")
do {
    func render(visible: Bool, gate: Probe, hit: Probe) -> [String] {
        makeList {
            if visible {
                if gate.eval(true) {
                    hit.eval("inner")
                }
            }
        }
    }

    for _ in 1...2 {
        let shut = closedHit.use { closedGate.use { render(visible: false, gate: closedGate, hit: closedHit) } }
        let shown = openHit.use { openGate.use { render(visible: true, gate: openGate, hit: openHit) } }
        print("closed \(shut.count), open \(shown.count)")
    }
}
for probe in [closedGate, closedHit, openGate, openHit] {
    probe.report()
}
```

```
closed 0, open 1
closed 0, open 1
closed-inner-condition: 0 evaluations, 2 uses -> never
closed-inner-line: 0 evaluations, 2 uses -> never
open-inner-condition: 2 evaluations, 2 uses -> on-every-use
open-inner-line: 2 evaluations, 2 uses -> on-every-use
```

When the outer condition is `false`, neither the inner condition nor the inner line is evaluated. `closed-inner-condition` and `closed-inner-line` record zero evaluations against two uses each and say `never`; the `closed 0` line in the output also shows that the list stayed empty. When the outer condition is `true`, both inner expressions are evaluated once on every call: `open-inner-condition` and `open-inner-line` record two evaluations each and say `on-every-use`. The `open 1` line confirms that the inner line entered the list. The order of the conditions behaves like short-circuiting here as well: the next expression is evaluated only if the path of the previous one is open. The difference is that this comes not from the `&&` operator but from the `buildOptional` path of the builder. Each `if` produces its own `buildOptional` call; if the part of the outer call is empty, the inner one is never reached, and the inner expression stays `never` for that reason.

## The Empty Loop

`buildArray` takes the parts that come from each iteration of the loop. If the loop never runs, the list of parts is empty and `buildArray` produces an empty list. The question is whether the expression in the body of a loop built over an empty array is evaluated. The measurement runs the same closure once with an empty array and once with a filled one.

```swift
// main.swift — the loop body over an empty array is not evaluated
let emptyLoop = Probe("empty-loop")
let filledLoop = Probe("filled-loop")
do {
    func render(names: [String], probe: Probe) -> [String] {
        makeList {
            for name in names {
                probe.eval(name)
            }
        }
    }

    for _ in 1...2 {
        let none = emptyLoop.use { render(names: [], probe: emptyLoop) }
        let some = filledLoop.use { render(names: ["x", "y"], probe: filledLoop) }
        print("empty \(none.count), filled \(some.count)")
    }
}
emptyLoop.report()
filledLoop.report()
```

```
empty 0, filled 2
empty 0, filled 2
empty-loop: 0 evaluations, 2 uses -> never
filled-loop: 4 evaluations, 2 uses -> on-every-use
```

In the loop over the empty array, the expression of the body never runs: `empty-loop` records zero evaluations against two uses and says `never`. The `empty 0` line says the result is an empty list as well; `buildArray` turns an empty list of parts into an empty result and gives no error. In the filled array each call processes two elements, so `filled-loop` records four evaluations over two calls and says `on-every-use`. The number of evaluations equals the number of iterations of the loop. For someone who builds a loop in the body of a builder, this is a measure: a loop that processes elements with an expression that has side effects runs as many times as there are elements on every call. The collection is not kept between calls; if the repetition of the side effect is not needed, the loop is moved outside the builder and the result is given to the builder ready-made.

The shared lesson of these measurements is this: the builder turns each line of the body into a call, and the way it is written decides which call is made. Instead of predicting by reading whether an expression will be evaluated, the transformation has to be unfolded in the mind: an expression becomes a `buildExpression` call, a condition becomes a `buildOptional` call (`buildEither` if there is an `else`), and a loop becomes a `buildArray` call. Before putting an expression with side effects into the body, the question is asked how many times and under which condition it will run in this chain of calls.

## The Bounding Measurement

The transformation is done at compile time, but which branch is chosen is decided at run time. In the previous measurements the condition was fixed and this difference was not visible. The last measurement turns the condition into a parameter and calls `render` in turn with `true`, `false` and `true`. The expression of each branch is written to a separate `Probe`.

```swift
// main.swift — the branch is chosen at run time
let left = Probe("left-branch")
let right = Probe("right-branch")

do {
    func render(first: Bool) -> [String] {
        makeList {
            if first {
                left.eval("left")
            } else {
                right.eval("right")
            }
        }
    }

    for flag in [true, false, true] {
        let lines = left.use { right.use { render(first: flag) } }
        print(lines.joined(separator: ","))
    }
}

left.report()
right.report()
```

```
left
right
left
left-branch: 2 evaluations, 3 uses -> on-every-use
right-branch: 1 evaluations, 3 uses -> on-first-use
```

The calls write `left`, `right`, `left` in turn. `left-branch` records two evaluations: it runs only in the two calls that come with `true`. `right-branch` records a single evaluation, and because this evaluation happened in the second call, after the first use, the rule says `on-first-use`. An expression is evaluated in the call in which its branch is chosen; being written in the code means nothing on its own. For this reason the `never` moment holds only when the branch is never chosen, and it depends on the condition.

**MA5.** The measured builder only builds a list of strings, and extra methods such as `buildLimitedAvailability` were not defined. That the same rule holds for `if`/`else if` chains with two or three branches was not measured. Using a single builder type is a design choice.

## Summary

- A result builder combines the expressions in a closure body into a result with the methods `buildBlock`, `buildExpression`, `buildOptional`, `buildEither` and `buildArray`.
- An expression in an unchosen branch of `if`/`else` is never evaluated; being written does not call it.
- The body of the builder runs again from the start on every call, and a `for` loop evaluates every element on every call.
- The transformation is done at compile time, but which branch is chosen is decided at run time; the `never` moment holds only if the branch is never chosen.
- This lesson's moment: **never**. The expression of an unchosen branch is not evaluated at any moment.

## Next Step

The first lessons of this topic measured memory and evaluation moments in code that runs in a single flow. New questions appear when several jobs touch the same memory at the same time: whose control a value is under, and who reaches it and when. The next lesson covers actors, `Sendable`, which stands for a type that can be passed across boundaries, and `@MainActor`; it measures how the lazy property of an actor behaves on first access, and the work started with `async let`.
