[Pitch] Lift all limitations on variables in result builders

Introduction

Implementation of result builder transform (introduced by SE-0289) places a number of limitations on local variable declarations, specifically: all declarations should have initializer expression, cannot be computed, have observers, or attached property wrappers. None of the uses described above are explicitly restricted by SE-0289.

Motivation

Result builder proposal describes how individual components in a result builder body are transformed, and it states that local declaration statements are unaffected by the transformation, which implies that all declarations allowed in context should be supported but that is not the case under current implementation that requires that declarations to 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 an attached property wrapper (with or without 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 properties with an explicit initializer expression.

Local variable declarations with property wrappers (with or w/o explicit initializer) could 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
        }
    }
}

Proposed solution

I propose to treat local variable declarations in the result builder bodies as-if they appear in a function or a multi-statement closure without any additional restrictions.

Detailed design

The change is purely semantic, without any new syntax. It allows declaring:

  • uninitialized
  • default initialized
  • computed
  • observed
  • property wrapped
  • lazy

properties in the result builder bodies and treat them just like they are treated in regular functions and closures, which means all of the semantics checks to verify their validity would still be performed and invalid (based on existing rules) declarations are still going to be rejected by the compiler.

Uninitialized variables are of particular interest because they require special support in the result builder as stated in SE-0289 otherwise there is no way to initialize them.

Source compatibility

This is an additive change which should not affect existing source code.

Effect on ABI stability and API resilience

These changes do not require support from the language runtime or standard library.

10 Likes

Does this also cover declaring variables in guard let?

guard is a statement which would be still controlled by result builder transformed, this is only about local declarations.

Does this proposal also lift the restriction on _ = expression statements, which can currently be worked around by prepending a let as in this SwiftUI example:

struct Demo: View {
    var body: some View {
        let _ = Self._printChanges()  // works
        _ = Self._printChanges()  // currently fails to compile
        Text("Hello world")
    }
}
1 Like

This is actually up to a particular result builder implementation because _ = <expr> is not a declaration but an assignment expression, which means that builder has to declare buildExpression(_: Void) to support that as per result builder proposal.

6 Likes

Didn't realise! Thanks for the insight!

1 Like

This pitch is now in review as SE-0373.