Result Builders
|
This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline. This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
A result builder is a type annotated @resultBuilder whose static build… methods tell the compiler how to
turn an ordinary-looking block of statements into a single built-up value — the mechanism behind SwiftUI’s
@ViewBuilder, but usable for any tree- or sequence-shaped value a domain-specific block syntax should produce.
@resultBuilder and buildBlock
@resultBuilder
struct ArrayBuilder<Element> {
static func buildBlock(_ components: Element...) -> [Element] {
components
}
}
@ArrayBuilder<Int>
func makeNumbers() -> [Int] {
1
2
3
}
makeNumbers() // [1, 2, 3]
Applying @ArrayBuilder<Int> to a function (or a closure parameter, via @ArrayBuilder<Int> () → [Int]) tells
the compiler to rewrite every statement in its body as an argument to ArrayBuilder<Int>.buildBlock(_:) rather
than evaluate them as ordinary top-level expression statements — 1; 2; 3 becomes
buildBlock(1, 2, 3), whose return value is what the function actually returns.
buildOptional, buildEither and buildArray
@resultBuilder
struct StringBuilder {
static func buildBlock(_ components: String...) -> String {
components.joined(separator: "\n")
}
static func buildOptional(_ component: String?) -> String {
component ?? ""
}
static func buildEither(first component: String) -> String { component }
static func buildEither(second component: String) -> String { component }
static func buildArray(_ components: [String]) -> String {
components.joined(separator: "\n")
}
}
@StringBuilder
func describe(_ n: Int) -> String {
"n is \(n)"
if n.isMultiple(of: 2) {
"n is even"
}
if n > 0 {
"positive"
} else {
"non-positive"
}
for i in 0..<n {
"item \(i)"
}
}
An if with no else compiles to buildOptional(:) (the branch’s value, or nil when it doesn’t run); an
if/else (or a switch) compiles to buildEither(first:)/buildEither(second:) depending on which branch
actually executed, so both branches must build to the same result type even though only one runs; a for loop
compiles to buildArray(:) over the array of per-iteration results. Each is optional — a builder that never
uses conditionals or loops need not implement them at all, and the compiler reports a clear diagnostic at the
call site of any control-flow construct its builder doesn’t support.
buildExpression, buildPartialBlock and buildFinalResult
@resultBuilder
struct HTMLBuilder {
static func buildExpression(_ value: String) -> String {
value // a hook to convert/validate each individual statement's value before it reaches buildPartialBlock
}
static func buildPartialBlock(first: String) -> String {
first
}
static func buildPartialBlock(accumulated: String, next: String) -> String {
accumulated + "\n" + next // folds statements left to right instead of taking one variadic buildBlock
}
static func buildFinalResult(_ component: String) -> String {
"<html>\n\(component)\n</html>" // a last transformation applied only once, to the whole built value
}
}
@HTMLBuilder
func page() -> String {
"<h1>Title</h1>"
"<p>Body</p>"
}
buildExpression(:) lets a builder validate or convert each statement’s value individually (e.g. wrapping a
String into a richer node type) before it is combined with its siblings. buildPartialBlock(first:) /
buildPartialBlock(accumulated:next:) is the modern (Swift 5.4+) alternative to a single variadic buildBlock:
the compiler folds statements pairwise, left to right, which both avoids the combinatorial overload sets a
variadic buildBlock needs to support every argument count and allows heterogeneous, per-position result types
(exactly how ViewBuilder types each position of a SwiftUI body distinctly). buildFinalResult(:) applies one
last transformation to the fully combined value, letting the builder’s public return type differ from the type
its internal combination methods work with (here, wrapping the assembled body in <html> tags exactly once).
Building a Small DSL
@HTMLBuilder
func greeting(named name: String, isVIP: Bool) -> String {
"<h1>Welcome, \(name)</h1>"
if isVIP {
"<p>You have VIP access.</p>"
} else {
"<p>Standard access.</p>"
}
}
print(greeting(named: "Ada", isVIP: true))
The payoff of a result builder is exactly this: a block of ordinary Swift statements — string literals, if,
for — reads like a small embedded language for the domain at hand (here, an HTML fragment builder; the same
shape works for validation rules, SQL fragments, or a routing table), while every branch is still type-checked
Swift code, not a string template. SwiftUI’s @ViewBuilder (an ordinary result builder over the View protocol
using buildPartialBlock/buildEither/buildOptional the same way) is the reason body { … } closures read
as a flat list of views — it is mentioned here only as the most widely recognized example of the pattern;
SwiftUI itself is out of scope for this reference.
The compiler performs this rewriting entirely at compile time: a builder-annotated function’s body is never
executed as ordinary sequential statements, only as the chain of build… calls the diagram above shows, which
is why a builder can support only the specific control-flow constructs whose corresponding build… method it
actually implements.
See Also
-
Closures —
@escaping/@Sendableclosures, which a result-builder parameter is itself one variety of. -
Attributes and Compiler Control —
@resultBuilderalongside Swift’s other attributes. -
Macros — a different compile-time code-transformation mechanism, generating or rewriting declarations rather than combining statement values.
-
Functional Programming — higher-order functions and composition, the style result builders are most often layered on top of.