Swift Digest
SE-0373 | Swift Evolution

Lift all limitations on variables in result builders

Proposal
SE-0373
Authors
Pavel Yaskevich
Review Manager
John McCall
Status
Implemented (Swift 5.8)

01 何が問題だったのか

result builder(SE-0289 で導入)で変換される関数本体の中では、ローカル変数に対して元の提案には書かれていない細かな制約がいくつかありました。具体的には、次のような変数宣言が事実上使えませんでした。

  • 初期化式のない var(例: let outcome: Outcome のように後から代入するパターン)
  • let (a, b) = ... のような複数名のタプル束縛
  • computed property(getter/setter を持つ)
  • willSet/didSet などのオブザーバ付き変数
  • property wrapper(@Clamped@UserDefault など)を付けた変数
  • lazy 変数

たとえば次のようなコードは、現在の result builder の中ではコンパイルエラーになります。

func compute() -> (String, Error?) { ... }

func test(@MyBuilder builder: () -> Int?) { ... }

test {
  let (result, error) = compute()  // 複数名の束縛は不可

  let outcome: Outcome              // 初期化式のない宣言は不可

  if let error {
    outcome = .failure
  } else {
    outcome = .success
  }

  switch outcome { ... }
}

また、SwiftUI のビュー本体のように result builder に囲まれた場所で、入力値の検証・整形や UserDefaults へのアクセスを property wrapper で表現したい場面もあります。

struct ContentView: View {
    var body: some View {
        GeometryReader { proxy in
            @Clamped(10...100) var width = proxy.size.width
            Text("\(width)")
        }
    }
}

こうした書き方は、通常の関数なら問題なく書けるにもかかわらず、result builder の中というだけで弾かれていました。SE-0289 本体はローカル宣言を変換対象外と位置づけていたため、この制約は仕様ではなく実装上の副作用として残っていた形です。

02 どのように解決されるのか

result builder で変換される関数の中のローカル変数を、通常の関数の中にある場合とまったく同じように扱うようにします。新しい構文は導入されず、純粋に意味論上の変更です。

これにより、次のすべての形の変数宣言が result builder クロージャ内でも書けるようになります。

  • 初期化式のない変数(後述の条件付き)
  • デフォルト初期化される変数(例: オプショナル型)
  • computed property
  • オブザーバ(willSet / didSet)付き変数
  • property wrapper 付き変数
  • lazy 変数

挙動は通常の関数と同じで、無効な宣言は従来どおりコンパイラに弾かれます。冒頭の例もそのまま書けるようになります。

test {
  let (result, error) = compute()

  let outcome: Outcome
  if let error {
    outcome = .failure
  } else {
    outcome = .success
  }

  switch outcome { ... }
}

property wrapper を使ったパターンも、result builder の中で自然に書けます。

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
        }
    }
}

初期化式のない変数を使う場合の条件

ひとつだけ例外的な条件があります。宣言時に初期化しない変数に後から値を代入する書き方は、代入式が Void を返すため、result builder 側で Void 型の結果を受け取れる必要があります。具体的には、buildExpressionVoid を受け取れるか、または buildBlockVoid を含む形で定義されている必要があります。

result builder が Void 結果をサポートしていない場合、その builder で変換される関数の中では初期化式のない宣言は引き続き使えません。@resultBuilder を定義する側が幅広い書き方を許したいときは、Void を受け付けるオーバーロードを用意しておくとよいでしょう。