[Swift] SE-0289 Result Builder を理解するために写経してみる

Swift

Function Builder/Result Builder をよく理解するために、SE-0289 を和訳/意訳してみました。

元ネタ

もちろん、これです。

以下、翻訳時の自分ルールです。

  • 専門用語は、できるだけ英語のままにする(さらに深く調べる時に Google の検索用語にしやすいハズ)
  • 状況の例示は、できるだけ意訳する
  • うまく意訳できないなら 愚直に翻訳 or スキップ (重要でないと判断したらスキップです)
  • 文脈依存で上記ルールは変動します・・・・

オリジナルと同じ章立てにしているので、怪しい文章はオリジナルを参照ください。

MEMO
この和訳/意訳は、英語のオリジナルを読みながら作成したもので、具体的には ブロックごとに1度読んだものを その直後に 和訳したものです。若干の前後の和訳での不整合は調整していますが、基本的には、前のブロックに戻って和訳の調整はしていません。
最後まで訳し終えた後に、全体を振り返って推敲を行うとよくなることは明らかですが、ある程度理解できてしまったので、手間をかけるモチベーションが減ってしまいました。
リクエストいただければ 推敲して訳をアップデートするかもしれません。

訳してみての感想

# ほかの言語をあまり詳しく知りません

この仕様がよく考えたうえで作られたことが見えたので あらためて、Swift という言語が好きになりました。

WWDC2021 で発表された Concurrency も 非常によく考えられた仕様だとおもうので、時間をみて、SE-0296SE-0304 を読んでみようと考えてます。

# SE-0304 は、Swift5.5 では未実装のようです。

Result builders

Changes from the first revision

スキップ

Introduction

この提案は、result builders について説明するものです。result builders は、component の配列から result value を 明示せずに 構築するものです。

ベースのアイデアは、以下のようなものです。


// Original source code:
@TupleBuilder
func build() -> (Int, Int, Int) {
  1
  2
  3
}

// This code is interpreted exactly as if it were this code:
func build() -> (Int, Int, Int) {
  let _a = TupleBuilder.buildExpression(1)
  let _b = TupleBuilder.buildExpression(2)
  let _c = TupleBuilder.buildExpression(3)
  return TupleBuilder.buildBlock(_a, _b, _c)
}

この例では、全ての statement が式であり、それぞれ 1つの value を作っています。他の statement (let や if, while 等) はそれぞれ扱いが異なったり、使用できなかったりします。詳細は、提案詳細を参照のこと。

実際に、この提案は、builder transformation を function の statement に適用することで、Swift を使った DSL(domain specific language) を作ることができるようになります。builder transformation の効力は original-code の動的な意味を保持することができるように、意図的に制限されています。例えば、original の code は、これまで通り実行することができ、通常は破棄されてしまう結果を result にあつめられます。builder transformation で一時的な protocol を使用することで、将来的な拡張の余地を残していて、新しい種類の statement をサポートすることや transformation の細かい点をカスタマイズすることができます。同様の builder pattern は、SE-0228 (文字列補完)でうまく使われています。

result builder は、Swift5.1 以来 "function builder" という名前で "隠された” feature として存在していて、改善され続けています。一番有名なものは SwiftUI の UI 記述のために使われているものですが、その他にも 使われています。GitHub の awesome function builders リポジトリを見ると たくさんの応用があることがわかります。

Motivation

すばらしいライブラリをつくることができるようにすることは、 Swift の重要なゴールです。素晴らしいライブラリは、インターフェースから作られ、Swift は、表現豊かで、type-safe であるようなライブラリ用インターフェース を作りやすくするための豊富な affordance とともにデザインされています。いくつかのケースでは、ライブラリーのインターフェースは、十分でわかりやすい Swift で記述した自身の ミニチュア言語を形作っています。このことを DSL(Domain Specific Language) と呼び、この DSL によって、特定の問題領域でのソリューションが記述しやすくなります。

result builder は、多くの問題領域でよく使われる list や tree 等の定義を必要とする特定のインターフェースをターゲットにしていて、それは、XML や JSON 等の構造化されたデータも含みますし、上で少し触れた Apple のSwiftUI で見られるような UI のビュー階層データ等も含みます。この提案では、HTML での DOM を生成するコードを例に説明します。

HTML を生成する箇所に問題があることを想定してください。もちろん、ダイレクトに String を生成できます。しかし、それは 誤りが発生しやすい方法です。文字列のエスケープやタグの対応づけ等を確認しなくてはいけません。String を直接生成することは、構造的に処理することも難しくします。別の方法として DOM のような表現を一度作成し、それを String に変換するというアプローチがあります。


protocol HTML {
  func renderAsHTML(into stream: HTMLOutputStream)
}

extension String: HTML {
  func renderAsHTML(into stream: HTMLOutputStream) {
    stream.writeEscaped(self)
  }
}

struct HTMLNode: HTML {
  var tag: String
  var attributes: [String: String] = [:]
  var children: [HTML] = []

  func renderAsHTML(into stream: HTMLOutputStream) {
    stream.write("<")
    stream.write(tag)
    for (k, v) in attributes.sort { $0.0 < $1.0 } {
      stream.write(" ")
      stream.write(k)
      stream.write("=")
      stream.writeDoubleQuoted(v)
    }
    if children.isEmpty {
      stream.write("/>")
    } else {
      stream.write(">")
      for child in children {
        child.renderAsHTML(into: stream)
      }
      stream.write("")
    }
  }
}

HTML の階層を簡単につくれるように、便利な関数を作ることにします。


func body(_ children: [HTML]) -> HTMLNode { ... }
func division(_ children: [HTML]) -> HTMLNode { ... }
func paragraph(_ children: [HTML]) -> HTMLNode { ... }
func header1(_ text: String) -> HTMLNode { ... }

しかし、便利な関数を作っても、複雑階層を構築するとすると まだわかりにくいままです。


return body([
  division([
    header1("Chapter 1. Loomings."),
    paragraph(["Call me Ishmael. Some years ago"]),
    paragraph(["There is now your insular city"])
  ]),
  division([
    header1("Chapter 2. The Carpet-Bag."),
    paragraph(["I stuffed a shirt or two"])
  ])
])

最初の問題は、句読点(,(),[] 等) が多くあることです。この問題は小さな問題かもしれません。しかし、減らすことができれば 句読点ではなく、コードの中身に集中できます。

2つ目の問題は、配列の記法を使っているため、型チェックを有効に使うためには、同じ型であることが必要となります。HTML の例では、同じ型に制限することは可能かもしれませんが、一般に適用すると問題になります。例えば、SwiftUI は、この型情報を使用してビュー階層の様々な最適化を行っているためです。

最大の問題は、階層構造を変更することが扱いにくいことです。白鯨 のようにその内容が固定であるならば、この問題は発生しませんが、実際には、情報は動的に変更され、大きく変わることが考えられます。例えば、chapter のタイトルを非表示にすることができるようにしたい時には、コードを以下のように変更する必要があります。


division((useChapterTitles ? [header1("Chapter 1. Loomings.")] : []) +
    [paragraph(["Call me Ishmael. Some years ago"]),
     paragraph(["There is now your insular city"])])

このような構造にすると これまでのコーディングに対する知見を適用することも難しくなります。例えば、何度も使う文字列があるとすると おそらく その文字列を変数として持つようにします。


let chapter = spellOutChapter ? "Chapter " : ""
  ...
header1(chapter + "1. Loomings.")
  ...
header1(chapter + "2. The Carpet-Bag.")

通常、変数のスコープは使われる範囲に絞ってできるだけ小さくすることが推奨されます。しかし、全体の階層が expression として定義されていて、expression 中に変数を定義することも難しいですし、狭いスコープで変数を定義することも難しいです。(実は、closure を使う方法がありますが、このことは1つ目の問題 句読点が増える等問題を加速させます)

それぞれを statement にしてそれを組み上げることをすると 部分的には解決することができます。


let chapter = spellOutChapter ? "Chapter " : ""

let d1header = useChapterTitles ? [header1(chapter + "1. Loomings.")] : []
let d1p1 = paragraph(["Call me Ishmael. Some years ago"])
let d1p2 = paragraph(["There is now your insular city"])
let d1 = division(d1header + [d1p1, d1p2])

let d2header = useChapterTitles ? [header1(chapter + "2. The Carpet-Bag.")] : []
let d2p1 = paragraph(["I stuffed a shirt or two"])
let d2 = division(d2header + [d2p1])

return body([d1, d2])

しかしこの方法は 問題を悪化させてしまいます。追加のコードが増えることで、何が起こっているかを追うことが難しくなります。親子関係を確認することも非常に難しく さらには、コードが定型文に近くなるので、コピーペーストバグも起こりやすくなります。さらにさらに 元のコードでは構造が分かりやすかったのですがこのコードではその情報は失われています、構造的に定義していたものを分解しているためです。optional な子要素は階層構造の典型的な問題で、チェックするためのコードが繰り返されることになり、boilerplace が増えて バグにつながります。まとめると、このアプローチはまったくお勧めできるものではありません。

結局、以下のようにまとめることができます。

  • 普通のコードのブロックを使って組み上げることができる自由度が欲しい。つまり、ローカルに定義することができ、コントロールフローを明示的に制御できること
  • (式の演算子を使って 組み上げることができることからくる)明示的な再帰構造と 暗黙的なデータフロー

この提案では、そのままの解決策を提案します。(訳註:意味が取れないのでスキップした文あり)
以下のような形でノード階層を定義できるようにするのがゴールです。


return body {
  let chapter = spellOutChapter ? "Chapter " : ""
  division {
    if useChapterTitles {
      header1(chapter + "1. Loomings.")
    }
    paragraph {
      "Call me Ishmael. Some years ago"
    }
    paragraph {
      "There is now your insular city"
    }
  }
  division {
    if useChapterTitles {
      header1(chapter + "2. The Carpet-Bag.")
    }
    paragraph {
      "I stuffed a shirt or two"
    }
  }
}

上のコードは、通常のコードに含まれるものでなければいけないので、一番外側の部分は通常の言語のルールに沿っていなければいけません。通常の言語のルールでは、上記のコードは、body という関数呼び出しをしていて、trailing closure が渡されていると考えられる。ここまでは OK、そしてbody に渡された 無名関数に対し 何らかの transformation を適用するということを行う。こう考えると、いくつかの疑問が発生してくる。

  • transformation のきっかけとなるのは何か? closure の契機を明示することが必要でない記法を選択しているので、きっかけがわからない。別の考え方は、Alternative Considered で議論されている。いずれにしても、body に closure を渡しているので、この問題は解決されないといけない
  • transformation は、多くの情報を集めるとすると、その情報は、どうやって body に返されるのか? transformation は、body 内で動いているので、あつめられた情報は 関数返り値で渡されるのが自然と思える。しかし、通常の return には、この transformation を 同時並行してサポートする要件は存在しない。
  • transformation が一連の部分的な情報を集めた時に、return で返すために どのようにしてそれらを結合したもの作るのか?tuple を作ることもできるが、通常呼び出し側が期待するものではない。このことは、異なる部分的な情報に対して異なる意味を期待するような DSL を許容するためには、有効であろう。理論的に考えると DSL から、部分的な情報を結合するための関数が提供されるべきであり、それは、一般的かつ上書きされるかもしれない。
  • transformation が部分的な情報を集める時に、その情報は、optional なコードの実行で集められる情報かもしれない(例 if) 。そのような時には、結合するための関数には、何が渡されるべきか?transformation は、部分的な情報の optional value を生成することができるかもしれないが、DSL は、optional として生成された 部分的な情報なのか 偶然にそれと一致したものなのかを区別する必要がある。理論的に考えると DSL から提供された関数によって、optional に提供された部分的な情報を通常の 部分的な情報に変換することができる必要がある。その変換により部分的な情報が 通常通り集められることが可能となる。

最後の2つの点は、DSL は型として通常の名前空間を提供することができる型

Detailed design

SE-0258 により、Swift に カスタムアトリビュートの概念が導入された。ここでどの概念を使用する。(訳註: property wrapper のこと)

Result builder types

result builder 型は、result builder として使用することができる型で、DSL として 関数の expression-statement から部分的な情報を収集し 返り値にまとめることができる。

result builder 型は、以下の2つの要件を満たす必要がある。

  • @resultBuidlder と 宣言されること。(そのことでresult builder として宣言されたとわかり、カスタムアトリビュートとしての使用が可能となる。)
  • 少なくとも1つの static な buildBlock という部分的な情報をまとめるための result-building メソッドが提供されること

追加すると、DSL が必要とする変換を行うために、多数の result-building メソッドが提供されるかもしれない。

Result builder attributes

result builder 型は、2つの異なる文法的なポジションでアトリビュートとして使うことができる。1つめは、func, var, subscript 定義として。var, subscript は、必ず getter を持つ必要があり、getter ではそれが アトリビュートとして存在しているかのように扱われる。result builder がこのように使われると、result builder による transform は、関数の body に適用される、つまり、関数の interface の一部としてはみなされないことで、ABI (Application Binary Interface) に影響を与えない。

result builder 型は、関数パラメータのアトリビュートとしても使うことができ、protocol の要件に含めることもできる。result builder がこのように使われると、closure が return しない限り、対応する引数として明示的に渡された closure の body に対して transform を適用することになる。この場合ではインターフェースの一部として扱われるので、ソースコードの互換性に影響を与えるが、ABI には影響を与えない。

Result-building methods

result builder として有意であるためには、result builder 型は、十分な result-building メソッドを 用意している必要がある。コンパイラーにより展開されたコードと result builder 型との間の protocol は、一時的で 任意に拡張できることが期待される。

result building メソッドは、static メソッドであることが必要で、result builder 型に対して呼ばれる。呼び出しは、BuilderType.<methodName>(<argument>) というコードが書かれているのと同様に型チェックされ、以下に書かれているようにラベル含め引数は以下のように記述される。つまり 通常のオーバーロードとその解決ルールが適用される。しかし、いくつかのケースでは result builder の transform は、result builder 型が特定のメソッドを定義しているかどうかによりその振る舞いを変る。このとき 相対的に弱いチェックしか行われないことは気をつけるべき点で、与えられた result builder 型では呼べないメソッドにより解決されるかもしれない。

以下は、result building method として提案されているものについての Quick reference である。ここでの型付けはわずかであり、マクロのような機能にみえる。以下の記述では、expression は、expression-statement として受け付けられる全てのタイプを表し、Component は、部分的 もしくは 結合された result の型を表す。 FinalResult は、最終的に transformed function から返され得る型を表す。

  • buildBlock(_ components: Component...) -> Component は、statement block の結合された result を構築するためにつかわれる。static method である必要がある。
  • buildOptional(_ component: Component?) -> Component は、実行結果として 部分的な result を持つか持たないか の時に使われる。(訳註:つまり optional な result) result builder が buildOptiona(_:) を提供するときは、transformed function に else なしの if を statement として含むことができる。
  • buildEither(first: Component) -> Component と buildEither(second: Component) -> Component は、selection statement により異なるパスで異なる result が生成されるときに使われる。result builder がこれらの method を提供する時には、transformed function は、else 付きの if や switch を含むことができる。
  • buildArray(_ components: [Component]) -> Component は、ループによって、複数の部分的な result から result を構築するために使われる。result builder が buildArray(_:) を提供する時には、transformed function は、for..in statement を含むことができる。
  • buildExpression(_ expression: Expression) -> Component は、expression-statement の結果を Component に格上げする時に使われる。提供は オプショナルであるが、提供された時には、statement-expression に対しての文脈を利用した型情報を提供したり、Component と Expression を区別することが可能となる。
  • buildFinalResult(_ component: Component) -> FinalResult は、最上位の関数の body のために、一番外側の buildBlock で構築された result を 確定させるために使われる。提供はオプショナルであるが、FinalResult を Component と区別することが可能となる。例えば、内部で処理に使用される型を外部に見せたくない時等が想定ユースケース。
  • buildLimitedAvailability(_ component: Component) -> Component は、限定的なコンテキスト(例えば if #available) にある buildBlock により構築された部分的な result を、コンテキストに合わせたものに transform するのに使われる。提供はオプショナルであり、内部の if #available による型情報を外部に渡すことが考えられるときのみ必要である。

要件は、以下の result builder の例のようにまとめることができる。


@resultBuilder
struct ExampleResultBuilder {
  /// The type of individual statement expressions in the transformed function,
  /// which defaults to Component if buildExpression() is not provided.
  typealias Expression = ...

  /// The type of a partial result, which will be carried through all of the
  /// build methods.
  typealias Component = ...

  /// The type of the final returned result, which defaults to Component if
  /// buildFinalResult() is not provided.
  typealias FinalResult = ...

  /// Required by every result builder to build combined results from
  /// statement blocks.
  static func buildBlock(_ components: Component...) -> Component { ... }

  /// If declared, provides contextual type information for statement
  /// expressions to translate them into partial results.
  static func buildExpression(_ expression: Expression) -> Component { ... }

  /// Enables support for `if` statements that do not have an `else`.
  static func buildOptional(_ component: Component?) -> Component { ... }

  /// With buildEither(second:), enables support for 'if-else' and 'switch'
  /// statements by folding conditional results into a single result.
  static func buildEither(first component: Component) -> Component { ... }

  /// With buildEither(first:), enables support for 'if-else' and 'switch'
  /// statements by folding conditional results into a single result.
  static func buildEither(second component: Component) -> Component { ... }

  /// Enables support for 'for..in' loops by combining the
  /// results of all iterations into a single result.
  static func buildArray(_ components: [Component]) -> Component { ... }

  /// If declared, this will be called on the partial result of an 'if
  /// #available' block to allow the result builder to erase type
  /// information.
  static func buildLimitedAvailability(_ component: Component) -> Component { ... }

  /// If declared, this will be called on the partial result from the outermost
  /// block statement to produce the final returned result.
  static func buildFinalResult(_ component: Component) -> FinalResult { ... }
}

The result builder transform

result builder transform は、statement block と その block に含まれる statement に再帰的に適用される。

Statement blocks

statement block の中では、statement は、個々に transform されて、statement 列になる。この statement 列は、1つの partial resuilt を構築するかもしれず、
TODO

Declaration statements

delcaration は、transformation によって、残されます。このことによって、result builder tranfromation に影響を与えずに、分離した expression を使うことができるようになります。(訳註:必要に応じてローカル変数を定義して使うことができるということ)

Expression statements

値を設定しない expression は、以下のように transform されます。

  • resuilt builder 型が resuilt-building method "buildExpression" を定義しているならば、transformation は それを呼びだし、expression statement を1つのラベルなしの引数として渡します。この呼び出しでの expression は、以降 expression statement として使われます。この呼び出しは、statement-expression と合わせて型チェックされ、その型に影響を与えます。
  • statement-expression は、let v = <expression> のように statement-expression の result 型変数を初期化するために使われます。この変数は、含まれている block の 部分 result として扱われます。この変数への参照は、別途 型チェックされるため、expression の型には、影響を与えません。
  • 部分的な result に どのようにして expression を反映できるかという点は、いくつかの領域の DSL にとっては、重要な点となる、例えば、ここで使っている HTML の例もそれに該当する。最初の HTML のサンプルでは、非常に少ない数の実装型を使った HTML protocol としていた。このようなケースでは、protocol を使うより enum を使って表現する方が Swift 的には自然であるが、<TODO: 文意不明>

    
    return body([
      division([
        header1("Chapter 1. Loomings."),
        paragraph([.text("Call me Ishmael. Some years ago")]),
        paragraph([.text("There is now your insular city")])
      ]),
      division([
        header1("Chapter 2. The Carpet-Bag."),
        paragraph([.text("I stuffed a shirt or two")])
      ])
    ])
    

    buildExpression を持つ DSL を使うと、自然な表現として enum を使うことができ、パターンマッチング等の利点を得られる。buildExpression をオーバーロードすることで、DSL の表現内で 一般的なケースを構築することが容易となる。

    
    static func buildExpression(_ text: String) -> [HTML] {
      return [.text(text)]
    }
    
    static func buildExpression(_ node: HTMLNode) -> [HTML] {
      return [.node(node)]
    }
    
    static func buildExpression(_ value: HTML) -> [HTML] {
      return 
    }
    

    Assignments

    assignment を実行する expression statement は、他の expression statement と同じように扱われるが、常に return () となる。resuilt builder は、() の扱いとして buildExpression をオーバーロードすることで特別に扱うことも可能である。

    
    static func buildExpression(_: ()) -> Component { ... }
    

    Selection statements

    if/else や switch statement は、場合に応じた値を生成する。selection statement に応じた tranformation のパターンが2つある。どちらの場合も例と示す。

    else のない if statement の場合:

    
    if i == 0 {
      "0"
    }
    

    この1つめの transfomration のパターンは、optional な partial result となる。この場合は、transformation を実施するコードはオプショナルで実行される。

    
    var vCase0: String?
    if i == 0 {
      vCase0 = "0"
    }
    let v0 = BuilderType.buildOptional(vCase0)
    

    2つめの tranformation のパターンは、1つの partial result にinjection することで、バランス木を生成する。このパターンは、if-else や switch をサポートする。以下のようなコードを例として考える。

    
    if i == 0 {
      "0"
    } else if i == 1 {
      "1"
    } else {
      generateFibTree(i)
    }
    

    上記のコードは、以下のようになる。

    
    let vMerged: PartialResult
    if i == 0 {
      vMerged = BuilderType.buildEither(first: "0")
    } else if i == 1 {
      vMerged = BuilderType.buildEither(second:
            BuilderType.buildEither(first: "1"))
    } else {
      vMerged = BuilderType.buildEither(second:
            BuilderType.buildEither(second: generateFibTree(i)))
    }
    

    selection statement の transformation は、次のように処理される。statement の 子 Block は、最初に解析され、場合の数が算出される。実装は、statement の複数階層を一度に解析することが許されている。例えば、case が if に含まれているケースで、(<TODO>)

    もし、N=0(result を構築する場合が無い)ならば、statement は、transformation の対象とならない。そうでなければ、injection strategy が選択される。

    • result builder 型が、buildEither(first:) と buildEither(second:) を定義しているならば、N 個の leaf を持つ 2分木を使う。それぞれの result は、leaf になる。これらの決定は、実装により定義される。新しい型の vMerged 変数は、statement よりも前に定義される。
    • そうでなければ、vCase 変数が result を生成するそれぞれの statement の前に定義される。

    transformation は、次のように実行される。

    • それぞれの result を生成する箇所で、transformation は、再帰的に適用される。各 case の final statement として、結合された result が inject され、外部に提供される。
      • statement が injection treeで使われていないときは、結合された result は、Optiona.Some で wrap され、適切な vCase に assign される。
      • そうで無い時には、injection tree の root から 適切な leaf までの経路が考慮される。 expression は、以下のようなルールで構築され、vMerged に assign される。
        • 経路が空であるときには、case からの オリジナルの 結合された result が使用される
        • tree の 左側のブランチでは、buildEither(first:) が、残りの経路の injection expression を引数として呼ばれたものが使用される
        • tree の 右側のブランチでは、同様に、buildEither(second:) が使用される
        • 最終的に、いずれかで result を生成しない case があるときは、expression は、Optional.some で wrap される。

        例えば、leaf までの経路が、left,left,right で、result を生成しない case があるとし、オリジナルの 結合された result が E だったとすると、vMerged に assign される inejction expression は、以下のようになる。

        
        Optional.some(
          BuilderType.buildEither(first:
            BuilderType.buildEither(first:
              BuilderType.buildEither(second: E))))
        

        vMerged に assign される全てのものは、一緒に type-check され、(訳註:<TODO>)

      • statement の後に、else のない if が存在したときは、v2 が buildOptional(_:) を v を引数として呼び出され初期化される。v2 は、block の 部分的な result である。そうでないときは、vMerged が用意され、vMerged が block の 部分的な result である。

    Imperative control-flow statements

    "result" statement が、transformation function 内に現れるのは、誤りである。しかし、transformation は、return を含む closure が使われる時があり得ることは、注意が必要である。したがって、func と getter が明示的に与えられた アトリビュートについてのみ適用されるルールとなる。

    "break" と "continue" も、transformation function 内に現れるのは、誤りである。これらの statement は、将来的に特定の状況でサポートされるかもしれない、例えば、全てが潜在的にスキップされるかもしれない 部分的な result を optional として扱うケースが考えられる。

    Exception-handling statements

    "throw" statement は、transformation によって、left 側に残される。

    "defer" statement は、transformation function 内に現れるのは誤りである。

    "catch" と組み合わせた "do" statement は、transformation function 内に現れるのは誤りである。

    do statements

    "catch" を持たない "do" statement は、block statement を囲っているもので、適切に transform される:

    • 変数 v が do に先駆けて定義される
    • 内部の statement block に transformation が再帰的に適用される
    • 子 Block の最後の statement が v にアサインされ、v は、block の部分的な result となる。

    for..in loops

    "for"..."in" statement は、ループを繰り返し、部分的な result を array に集める。その array は、buildArray に渡される。具体的には以下:

    • 変数 v が for に先駆けて定義される
    • vArray も for に先駆けて定義される。型は、Array で、[] として初期化される。
    • 関数内部の for in ループに transformation は、再帰的に適用される。ただし、生成された部分的な result は、vArray.append を使って追加される。
    • buildArray(vArray) の返り値が v にアサインされ、v は、block の 部分的 result になる。

    buildArray が提供されていない時には、for in は、サポートされない。

    Compiler Diagnostic Directives

    #warning と #error は実行時に影響はない。変更されずに残される。

    Availability

    コードの有効性を制限するための statement 例えば、if #available(...) を使うことで、ライブラリの古いバージョンとの互換性を向上させることができる。result builder は、(SwiftUI の ViewBuilder のように)完全な型情報を保持するため、buildLimitedAvailability を使用して、type を "erase" することが必要になるかもしれない。以下は、Paul Hudson から借用したサンプルコード。

    
    @available(macOS 10.15, iOS 13.0, *)
    struct ContentView: View {
        var body: some View {
            ScrollView {
                if #available(macOS 11.0, iOS 14.0, *) {
                    LazyVStack {
                        ForEach(1...1000, id: \.self) { value in
                            Text("Row \(value)")
                        }
                    }
                } else {
                    VStack {
                        ForEach(1...1000, id: \.self) { value in
                            Text("Row \(value)")
                        }
                    }
                }
            }
        }
    }
    

    LazyVStack は、macOS11/iOS14 で導入された。しかし この View 自体を、macOS10.15/iOS13 でも使用可能にするために、if #available を使用している。SwiftUI は、view builder closure で完全な型情報を保持していて、次のような条件付きのブランチも含んでいる

    
    static func buildEither(first: TrueContent) -> _ConditionalContent
    

    このことは、ScrollView は、macOS10.15/iOS13 でも LazyVStack を参照していることを意味していて、つまり、コンパイルエラーとなる。buildLimitedAvailability は、result builder に対して、通常保持する型情報を "erase" する方法を提供する。特に以下のようなシチュエーションを想定している

    
    static func buildLimitedAvailability(_ content: Content) -> AnyView { .init(content) }
    

    以下のような if #available の箇所に注目してみる。

    
    if #available(macOS 11.0, iOS 14.0, *) {
        LazyVStack { }
    } else {
        VStack { }
    }
    

    これは、以下のように transform される。

    
    let vMerged: *inferred type*
    if #available(macOS 11.0, iOS 14.0, *) {
        let v0 = LazyVStack { }
        let v1 = ViewBuilder.buildBlock(v0)
        let v2 = ViewBuilder.buildLimitedAvailability(v1)
        vMerged = ViewBuilder.buildEither(first: v2)
    } else {
        let v3 = VStack { }
        let v4 = ViewBuilder.buildBlock(v3)
        vMerged = ViewBuilder.buildEither(second: v4)
    }
    

    Example

    当初の例に戻り、result-builder DSL をどうやって定義するか考察してみる。最初に以下のように resuilt builder type を定義する。

    
    @resultBuilder
    struct HTMLBuilder {
      // We'll use these typealiases to make the lifting rules clearer in this example.
      // Result builders don't really require these to be specific types that can
      // be named this way!  For example, Expression could be "either a String or an
      // HTMLNode", and we could just overload buildExpression to accept either.
      // Similarly, Component could be "any Collection of HTML", and we'd just have
      // to make buildBlock et al generic functions to support that.  But we'll keep
      // it simple so that we can write these typealiases.
    
      // Expression-statements in the DSL should always produce an HTML value.
      typealias Expression = HTML
    
      // "Internally" to the DSL, we'll just build up flattened arrays of HTML
      // values, immediately flattening any optionality or nested array structure.
      typealias Component = [HTML]
    
      // Given an expression result, "lift" it into a Component.
      //
      // If Component were "any Collection of HTML", we could have this return
      // CollectionOfOne to avoid an array allocation.
      static func buildExpression(_ expression: Expression) -> Component {
        return [expression]
      }
    
      // Build a combined result from a list of partial results by concatenating.
      //
      // If Component were "any Collection of HTML", we could avoid some unnecessary
      // reallocation work here by just calling joined().
      static func buildBlock(_ children: Component...) -> Component {
        return children.flatMap { $0 }
      }
    
      // We can provide this overload as a micro-optimization for the common case
      // where there's only one partial result in a block.  This shows the flexibility
      // of using an ad-hoc builder pattern.
      static func buildBlock(_ component: Component) -> Component {
        return component
      }
      
      // Handle optionality by turning nil into the empty list.  
      static func buildOptional(_ children: Component?) -> Component {
        return children ?? []
      }
    
      // Handle optionally-executed blocks.
      static func buildEither(first child: Component) -> Component {
        return child
      }
      
      // Handle optionally-executed blocks.
      static func buildEither(second child: Component) -> Component {
        return child
      }
    }
    

    次に、ユーティリティ関数をいくつか作成する

    
    func body(@HTMLBuilder makeChildren: () -> [HTML]) -> HTMLNode {
      return HTMLNode(tag: "body", attributes: [:], children: makeChildren())
    }
    func division(@HTMLBuilder makeChildren: () -> [HTML]) -> HTMLNode { ... }
    func paragraph(@HTMLBuilder makeChildren: () -> [HTML]) -> HTMLNode { ... }
    

    最初のコード例に戻って見て、一部がどのように transform が機能するか見てみる。

    
    division {
      if useChapterTitles {
        header1(chapter + "1. Loomings.")
      }
      paragraph {
        "Call me Ishmael. Some years ago"
      }
      paragraph {
        "There is now your insular city"
      }
    }
    

    division に渡された closure の最上位の statement が 1つ1つ transform される。

    "if" statement は、then のある時と (暗黙的な) else のあるケースの2つ考えられる。then のあるときは、result を生成する。assign しない expression-statement がそこにあるため、result が生成される。then のあるケースとして、再帰的に transform する。

    
      if useChapterTitles {
        let v0: [HTML] = HTMLBuilder.buildExpression(header1(chapter + "1. Loomings."))
        // combined result is HTMLBuilder.buildBlock(v0)
      }
    

    else のない if なので、injection tree を使用していない。

    
      var v0_opt: [HTML]?
      if useChapterTitles {
        let v0: [HTML] = HTMLBuilder.buildExpression(header1(chapter + "1. Loomings."))
        v0_opt = v0
      }
      let v0_result = HTMLBuilder.buildOptional(v0_opt)
      // partial result is v0_result
    

    2つの paragraph は、特段明記しないが、処理に応じて、transform される。non-assignment expression-statement であるので、それらは、Component 型となる。

    
    division {
      var v0_opt: [HTML]?
      if useChapterTitles {
        let v0: [HTML] = HTMLBuilder.buildExpression(header1(chapter + "1. Loomings."))
        v0_opt = v0
      }
      let v0_result = HTMLBuilder.buildOptional(v0_opt)
      
      let v1 = HTMLBuilder.buildExpression(paragraph {
        "Call me Ishmael. Some years ago"
      })
      
      let v2 = HTMLBuilder.buildExpression(paragraph {
        "There is now your insular city"
      })
      
      // partial results are v0_result, v1, v2
    }
    

    最後に、buildBlock を呼び出すことで、この block の処理を終えることとなる。function の最上位であるので、buildFinalResult を定義せずに、直接返している。

    
    division {
      var v0_opt: [HTML]?
      if useChapterTitles {
        let v0: [HTML] = HTMLBuilder.buildExpression(header1(chapter + "1. Loomings."))
        v0_opt = v0
      }
      let v0_result = HTMLBuilder.buildOptional(v0_opt)
      
      let v1 = HTMLBuilder.buildExpression(paragraph {
        "Call me Ishmael. Some years ago"
      })
      
      let v2 = HTMLBuilder.buildExpression(paragraph {
        "There is now your insular city"
      })
      
      return HTMLBuilder.buildBlock(v0_result, v1, v2)
    }
    

    これで、closure は、完全に transform されたことにある。(paragraph に渡された ネストした closure を除いてですが)

    Type inference

    Result builder bodies

    resuilt builder での型推論は、result builder の transformation の構文から得られる。以下のような例で、result builder を適用してみる。

    
    {
      42
      3.14159
    }
    

    resuilt builder transformation は、以下のように変換する

    
    let v1 = 42
    let v2 = 3.14159
    return Builder.buildBlock(v1, v2)
    

    v1 と v2 の型は、独立して(通常の型推論ルールにより)推測され、Int と Double になる。そして、buildBlock は、closure の返り値を生成するために、それらを使用する。ただし、buildBlock は、v1 や v2 がどのように計算されるかについて影響を与えることはできない。例えば、以下のような buildBlock を持つ builder を考えてみる。

    
    func buildBlock(_ a: T, _ b: T) -> T { ... }
    

    このとき、buildBlock(v1, v2) はエラーとなる、なぜならば、Int と Double は異なる型であるためである。もし 型推論で v1 の情報に後から影響を及ぼすことができれば、Int である 42 を Double のように扱えるかもしれないが、、それらは、型としては異なるものである。

    Swift5.1 での最初の実装は、異なる型推論 transform を使用していたので、上記のような 型推論に影響を与えることが可能でした。例えば:

    
    return Builder.buildBlock(42, 3.14159)  // not proposed; example only
    

    このケースでは、42 は、Double として扱われました。上記のような推論に影響を与える手法が result Builder に適切でないとする理由はいくつかあります。

    • result builder の型推論モデルが、通常の closure や function のものとは違うと、理解するための心理的モデルがより複雑になってしまう
    • resuilt builder を使った時の型チェックのパフォーマンスが許容できなかった。なぜなら、型推論に影響を与えると計算量が指数的に増加してしまうためである。1方向に沿った制約を使った実装(現在の実装)では、SwiftUI で発生する "expression too complex to be solved in a reasonable time" の大半を解決することができました

    Inferring result builders from protocol requirements

    大部分の resuilt builder transformation は、result builder の API を書くことなく暗黙的に適用される。例えば、次のような API を考えてみる。

    
    func paragraph(@HTMLBuilder makeChildren: () -> [HTML]) -> HTMLNode { ... }
    

    result builder "HTMLBuilder" は、引数がマッチした時には 呼び出し箇所それぞれに、暗黙的に適用されます。

    
    paragraph {
      "Call me Ishmael. Some years ago"
    }
    

    大部分の関数定義は独立しているので、明示的な resuilt builder 宣言でのみ transform が可能となります。しかし、SwiftUI のような result builder DSL は、多くの異なる型に準拠するような 典型的なプロトコルを定義する。典型的な SwiftUI の ビューは、以下のようなビューである。

    
    struct ContentView: View {
      @ViewBuilder var body: some View {
        Image(named: "swift")
        Text("Hello, Swift!")
      }
    }
    

    ほぼすべての SwiftUI の View のもつ body には、@ViewBuilder を使うことができる、なぜなら body は、 View を定義し、それらは、ViewBuilder で構築される。View プロトコル 自体の body に @ViewBuilder を付与して定義することで、すべての body に @ViewBuilder を記述するという 冗長なコードの繰り返しを避けることができる。

    
    protocol View {
      associatedtype Body: View
      @ViewBuilder var body: Body { get }
    }
    

    View に conform した型が body を定義した時には、プロトコル から、@ViewBuilder アトリビュート 推測され、暗黙的に resuilt builder の transform が適用される。これは、以下のようなケースでない限り適用される。

    • 関数かプロパティに result builder アトリビュートが明示的に 設定されているとき もしくは、
    • 関数の body か プロパティの getter が明示的な return を持っているとき

    Implicit memberwise initializer

    result builder は、composition を意識して設計されていて、子要素について記述する時には、複数の小さなブロックで構成されることを想定している。
    例えば、SwiftUI の VStack は、以下のようになる。

    
    struct CustomVStack: View {
        let content: () -> Content
    
        var body: some View {
            VStack {
                // custom stuff here
                content()
            }
        }
    }
    

    しかし、この VStack は、以下のような initializer を書かないと機能しない。

    
    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }
    

    resuilt builder のアトリビュートを stored property に付与することもできる。このことで、memberwise initializer の対応するパラメータに resuilt builder アトリビュートを付与することができる。つまり、以下のように変更することができる。

    
    struct CustomVStack: View {
        @ViewBuilder let content: () -> Content
    
        var body: some View {
            VStack {
                // custom stuff here
                content()
            }
        }
    }
    

    このことで、先に提示した memberwise initializer が暗黙的に定義されることになる。

    resuilt builder アトリビュートは、 structurally resemble function type でないタイプの stored property にも付与することができる。その場合は、
    以下を想定する。

    
    struct CustomHStack: View {
        @ViewBuilder let content: Content
    
        var body: some View {
            HStack {
                // custom stuff here
                content
            }
        }
    }
    

    暗黙的な memberwise initializer は、次のような定義となる。

    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    

    このアイデアは、SR-13188 としてレポートされ、サンプルコードも、そこから引用している。

    Source compatibility

    resuilt builder は、追加機能であるため、既存コードに影響を与えない。

    result builder のいくつかは、実装依存になっているため(例えば、switch に対しての ツリー構造)、DSL によっては、差異が出るように記述されている時には、将来バージョンでの振る舞いに相違点がでるかもしれない。

    Effect on ABI stability and API resilience

    訳註;興味ないのでスキップ

    Future Directions

    この提案のデザインを変更せずに 追加することができる機能がいくつかあります。そのうちのいくつかを列挙します。

    "Simple" result builder protocol

    Swift の forum で 全ての expression が同じタイプを持つようなすべての文法をサポートする result builder を定義することが容易になるように protocol を使用するデモが、@anreitersimon によりおこなわれた。
    サンプルコードは以下の通り(少し調整している)。ベースのアイデアは、result を記述する tree を形成することにある。

    
    enum Either {
      case first(T)
      case second(U)
    }
    
    indirect enum ResultBuilderTerm {
        case expression(Expression)
        case block([ResultBuilderTerm])
        case either(Either)
        case optional(ResultBuilderTerm?)
    }
    

    そして、 ResultBuilder protocol は、requirement として buildFinalResult を持つことのみが要求され、それは、すべての値から、final result を形成することである。

    
    protocol ResultBuilder {
        associatedtype Expression
        typealias Component = ResultBuilderTerm
        associatedtype FinalResult
    
        static func buildFinalResult(_ component: Component) -> FinalResult
    }
    

    他のすべての build メソッドは、追加的に if や switch 等を扱うことができるように、ResultBuilder の extension として実装される。

    
    extension ResultBuilder {
        static func buildExpression(_ expression: Expression) -> Component { .expression(expression) }
        static func buildBlock(_ components: Component...) -> Component { .block(components) }
        static func buildOptional(_ component: Component?) -> Component { .optional(component) }
        static func buildArray(_ components: [Component]) -> Component { .block(components) }
        static func buildLimitedAvailability(_ component: Component) -> Component { component }
    }
    

    そうすることで、あたらしいバージョンの builder を少ないコードで作成することが可能となる。例えば以下は、展開された result を array にする builder になっている。

    
    @resultBuilder
    enum ArrayBuilder: ResultBuilder {
        typealias Expression = E
        typealias FinalResult = [E]
    
        static func buildFinalResult(_ component: Component) -> FinalResult {
            switch component {
            case .expression(let e): return [e]
            case .block(let children): return children.flatMap(buildFinalResult)
            case .either(.first(let child)): return buildFinalResult(child)
            case .either(.second(let child)): return buildFinalResult(child)
            case .optional(let child?): return buildFinalResult(child)
            case .optional(nil): return []
            }
        }
    }
    

    もう少し検討を重ねると、このような要素は、standard library の一部になるかもしれない。

    以下、未訳

    未訳:Stateful result builders

    未訳:Transforming declarations

    未訳:Virtualized Abstract Syntax Trees (ASTs)

    未訳:Alternatives considered

    未訳:Additional control-flow statements

    未訳:Builder-scoped name lookup

    未訳:Dropping Void/Never values

    説明は以上です。
    不明な点やおかしな点ありましたら、こちらまで。

    コメントを残す

    メールアドレスが公開されることはありません。 * が付いている欄は必須項目です