- Proposal: SE-0373
- Author: Pavel Yaskevich
- Review Manager: John McCall
- Status: Implemented (Swift 5.8)
- Implementation: apple/swift#60839
- Review: (pitch) (review) (acceptance)
The implementation of the result builder transform (introduced by SE-0289) places a number of limitations on local variable declarations in the transformed function. Specifically, local variables need to have an initializer expression, they cannot be computed, they cannot have observers, and they cannot have attached property wrappers. None of these restrictions were explicit in the SE-0289 proposal, but they are a de facto part of the current feature.
The result builder proposal describes how the result builder transform handles each individual component in a function body. It states that local declarations are unaffected by the transformation, which implies that any declaration allowed in that context should be supported. That is not the case under the current implementation, which requires that local variables declarations must have a simple name, storage, and an initializing expression.
In certain circumstances, it's useful to be able to declare a local variable that, for example, declares multiple variables, has default initialization, or has an attached property wrapper (with or without an initializer). Let's take a look at a simple example:
func compute() -> (String, Error?) { ... }
func test(@MyBuilder builder: () -> Int?) {
...
}
test {
let (result, error) = compute()
let outcome: Outcome
if let error {
// error specific logic
outcome = .failure
} else {
// complex computation
outcome = .success
}
switch outcome {
...
}
}
Both declarations are currently rejected because result builders only allow simple (with just one name) stored variables with an explicit initializer expression.
Local variable declarations with property wrappers (with or without an explicit initializer) can be utilized for a variety of use-cases, including but not limited to:
- Verification and/or formatting of the user-provided input
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { proxy in
@Clamped(10...100) var width = proxy.size.width
Text("\(width)")
}
}
}
- Interacting with user defaults
import SwiftUI
struct AppIntroView: View {
var body: some View {
@UserDefault(key: "user_has_ever_interacted") var hasInteracted: Bool
...
Button("Browse Features") {
...
hasInteracted = true
}
Button("Create Account") {
...
hasInteracted = true
}
}
}
I propose to treat local variable declarations in functions transformed by result builders as if they appear in an ordinary function without any additional restrictions.
The change is purely semantic, without any new syntax. It allows variables of all of these kinds to be declared in a function that will be transformed by a result builder:
- uninitialized variables (only if supported by the builder, see below for more details)
- default-initialized variables (e.g. variables with optional type)
- computed variables
- observed variables
- variables with property wrappers
lazy
variables
These variables will be treated just like they are treated in regular functions. All of the ordinary semantic checks to verify their validity will still be performed, and invalid declarations (based on the standard rules) will still be rejected by the compiler.
There is one notable exception to this general rule. Initializing a variable after its declaration requires writing an assignment to it, and assignments require the result builder to support Void
results, as described in SE-0289. If the result builder does not support Void
results (whether with an explicit buildExpression
or just by handling them in buildBlock
), transformed functions will not be allowed to contain uninitialized declarations.
This is an additive change which should not affect existing source code.
These changes do not require support from the language runtime or standard library, and they do not change anything about the external interface to the transformed function.