ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
リザルトビルダーを使用したSwiftでのDSLの記述
いくつかの問題は、カスタマイズされたプログラミング言語、または "ドメイン固有の言語 "を作成することによって解決するのが簡単です。DSLを作成するには、慣習的に独自のコンパイラを記述する必要がありますが、代わりにSwift 5.4でリザルトビルダーを使用することで、コードを読みやすく、維持しやすくすることができます。Swift用のカスタム言語を設計するためのベストプラクティスをお伝えします。リザルトビルダーと末尾のクロージャ引数について学び、修飾子スタイルのメソッドとこれらがうまく機能する理由を検証し、Swiftの通常の言語規則を拡張してSwiftをDSLに変換する方法を紹介します。 このセッションを最大限活かしていただくためには、ある程度のSwiftUIビューの記述経験があることが望ましいです(必須ではありませんが)。パーサーやコンパイラの実装についての知識は一切必要ありません。
リソース
- Attributes - The Swift Programming Language
- Fruta: Building a Feature-Rich App with SwiftUI
- Result Builders - The Swift Programming Language
関連ビデオ
WWDC21
WWDC19
-
ダウンロード
♪ (リザルトビルダーを使用したSwiftでのDSLの記述) こんにちは Beccaです Swift Compilerのチームメンバーです 今日の講義は私で担当します SwiftでDSLを実装する方法を説明します その言葉を聞いたことがなければ DSLはドメイン固有言語という意味の略語です この用語を初めて体験しても おそらく以前に使用したことがあります 今 DSLが実際に何であるか を説明することから始めます さらに Swiftでどのように 表示するかを説明します 次に リザルトビルダーがどのように 機能するかを説明します 次に リザルトビルダーがどのように 機能するかを説明します その後 簡単なDSL設計の手順 について説明します Frutaは簡単なAppの一部です 最後に 実装の書き方を紹介します これはFrutaのコード用例にあります しかし その頭字語をもう少しよく 説明することから始めましょう DSLは一種のミニチュアプログラミング言語です これは特定の分野で機能する プログラム向けに設計されます これは「ドメイン」と呼ばれます この言語設計の目的は 特定種類の作業の考えを目標とします これは特別な機能であることも可能です 目標はそのような作業をより簡単にします したがって DSL用のコードを書く場合は 汎用言語の代わりに あなたは次のものだけを書く必要があります それは正確な問題に特定するものです たくさんのDSLは宣言型です つまり 正確な指示を実際に書く必要はありません 問題を解決するために それはユーザがその言語で問題 を説明しているようなものです すると それはユーザのために問題を解決します 従来 ユーザは次の方法を採用します これは「スタンドアロンDSL」と呼ばれます 言語全体を初めから設計します そのためにインタプリタか またはコンパイラを作成します 組み込みDSLはより現代的な代替手段です 内部DSLではビルドイン機能を使用します これはスイフトのようなホスト言語の DSLの暗黙の動作を追加するには ユーザのコードの一部に ホスト言語を有効的に変更する ドメインに合わせたものに これは明らかにもっと簡単です 全体言語を設計することより そしてそれのためにコンパイラを書きます 理由はユーザが既存の言語から始めたからです 言語構文の基本はすでに決まりました 且つ コンパイラもすでにあります また DSLコードの混合も簡単になります 非DSLコードと一緒に 多くの場合 DSLを使用して 問題を解決しようとします これは より大きなAppのほんの一部です スタンドアロンDSLを作成している場合は ある言語から別の言語に呼び出す 方法を設計する必要があります 内部DSLの場合DSLで記述された部分は Appの他の部分から見ると 通常のコードのように見えます そのため 相互運用がはるかに簡単になります 内部DSLは 設計された ツールを使用することもできます ホスト言語のために すでにSwift用のデバッガー とエディターを持っています これらはSwiftDSLでは問題なく機能します スタンドアロンDSLにそのものが必要な場合は 自分で書く必要があります そして ユーザはホスト言語から始めているので その言語をすでに知っているクライアント 学ぶ必要のことがかなり少なくなります 彼らはすでに変数を宣言する方法を知っています または 「elseif」にスペースが あるかどうかことを 学ぶ必要があるのは 言語を カスタマイズした方法だけです Swiftは 内部DSLを支援 するように設計されています 実際 SwiftUIを使用したことがある場合は すでに使用ししました SwiftUIビューDSLは 次 を記述したいことを想定します デバイスのスクリーン上の画面のレイアウト だから SwiftUI DSLで 記述している場合 カスタムコードは単にビューを作成し DSLはそれら以外からツリー を構築することを担当します SwiftUIに処理するために DSLの機能を理解するには SwiftUIがどのようなものに みえるか考えてください 代わりに通常のSwiftでビューを作成した場合 ビューを作成して変更し それらを他のビューに追加し最後に1つ返します いたるところに一時変数を作成する必要があります それぞれのビューを保持するために そしてその結果本当に伝えないでしょう DSLと同じ方法でビューがネストされる方法 より多くのコードを書きますが それはあまり意味を伝えません 対照的にSwiftUI DSLは これらの面倒な詳細をすべて暗黙的にします ビューを説明するのはユーザの仕事です 説明しているビューを 収集するのはDSLの仕事です そしてそれらを表示する方法を理解します しかし DSLは実装するのに 余分な努力を要します そしてそれらは使用するために余分な努力をします では いつ作成しますか? でも 厳格なルールはありませんが しかし ここにそれを使いたい かもしれない兆候があります バニラスウィフトを使用するメカニ ズムが存在する場所を探します コードの意味をあいまいにします 半分の時間をコンマの再配置に費やす場所 そして角括弧及び括弧 何かを変えるたびに または一時配列に物事を追加する必要があります 制御フローに対応します 最善のアプローチが行われる状況を探します コードの別の部分に何かを説明することです 直接指示を書く代わりに それをどうするかについて 例えば サーバー側のWebフレームワークでは ハンドラーを登録するエリアがあるかもしれません サポートするURLに対して addHandler方法を 何度も呼び出す代わりに クライアント用のDSLを設計できます 各URLとそのハンドラーを宣言するには その後 フレームワークはそれら を自動的に登録できます 維持されるコードの部分を探します 主な仕事がプログラミングではない人々によって たとえば テキストアドベンチャー ゲームを書いていると想定します ほとんどのコードで作業する 開発者はごくわずかですが ただし 部屋のマップはゲーム デザイナーによって更新されます NPCダイアログは著者によって追加されます 多分DSLは彼らの仕事をより簡単にするでしょう ユーザがたくさんのマイレージ を得る状況を探してください DSL外部からの ライブラリは良い例です 彼らは多くの異なるクライアント によって使用されるためです しかし DSLはプロジェクト内の 何かを処理することもできます ユーザがたくさん定義し または 頻繁に読んだり 更新したりする必要があるもののみ そして それをできるだけ簡単にしたいのです DSLを作成する理由 ユーザはそれだけではない事実 とバランスの必要があります 設計と実装に少し努力してください また クライアントが学習するの にもある程度の努力が必要です 方法と配列リテラルがDSLと ほぼ同じくらい優れている場合 多くの場合 正しい答えですが Swiftプログラマーは それらの使用方法をすでに正確に知っています しかし SwiftUIのように時々には DSLは正解です では どうやって作りますか Swift機能がどのように一緒に 使用されるかを分析しましょう SwiftUIDLSを構築するために Swiftの一般的にクリーンな構文に加えて SwiftUIDSLは4つの ことを利用しています プロパティラッパー これらにより クライアントは変数を宣言できます これはDSLの動作に関連しています 末尾のクロージャ引数 これらにより DSLは関数または 初期化子を提供できます ほとんどカスタム構文のように読める それが言語に追加されました リザルトビルダー これらは DSLのコードで 計算された値を収集します それらを処理できるように戻り値に変換します そして最後に修飾子スタイルのメソッド これらは基本的に ラップされた ものを返す単なるメソッドです または 呼び出された値の修正バージョン ビルダーが生成された以来 コードによって計算された値を収集し このパターンは彼らと本当にうまく機能します 現在 プロパティラッパーはすでに含まれています 2019年以来のこのセッションの後半では ですから 今日はそれらをあまり話しません しかし これらの他の3つ特に生まれたビルダーは このセッションの主要なトピックになります 末尾のクロージャと修飾子式の方法は 多くのSwiftプログラマーが 精通していることです しかし リザルトビルダーはより舞台裏の機能です それでは それらがどのように 機能するかについて話しましょう だから彼らと一緒にDSLの 構築を始めることができます リザルトビルダーは 値を収集する ために使用されます DSLで作成し それらをつなぎ合わせます どんなデータ構造にも言語がそれらを望むところ これらはプロパティラッパーに少し似ています 特別な型を宣言するところ 次に そのタイプを属性として使用できます 具体的にはリザルトビルダーを適用できます 戻り値を持つほとんどすべての関数本体に 関数やメソッドのように 計算されたプロパティの ゲッター またはクロージャ リザルトビルダーを関数本体に適用すると Swiftは静的メソッドに さまざまな呼び出しを挿入します リザルトビルダーで これらは最終的にステートメント のリザルトをキャプチャします そうでなければ破棄されたでしょう したがって Swiftが常に 戻り値を無視する場合 代わりに それはリザルトビルダーに渡されます これらの呼び出しは最終的に値を計算します この値は関数本体から返されます したがって 関数を呼び出すと 次のことが実行されます その関数内のすべてのステート メントは正常に機能します それらによって生成された値を収集し それらによって生成された値を収集し それがロージャのリザルトになります リザルトビルダーはコンパイル時の機能であるので それらはAppが実行される すべてのOSで動作します 機能の最終バージョン オープンソースのSwift Evolution提案289から Swift5.4に含まれます だから 直前の4月にXcode 12.5で出荷されました しかし その機能のプロトタイプは それ以前に利用可能になるため これはいくつかの古いチュートリ アルやライブラリを見る原因です プロトタイプを使用している それらは「関数ビルダー」と呼ばれるでしょう 「リザルトビルダー」の代わりに それらは最終的な機能と完全には 一致しない可能性があります そこで 以前SwiftUIDLS で使用された機能を指摘しました しかし 今は それらがどのように 機能するかについて話しましょう いくつかの詳細を簡略化しましょう-- 一部のSwiftUI型の無関係 な部分を削除するように 且つ v0のような偽の変数名を表示します コンパイラによって生成された変数の場合--- しかし これは基本を理解するのに役立つはずです まず明瞭のことはトップレベルでは 新しい構文のように見えるブロック を持つこのVStackは 実際に末尾のクロージャ引数です VStackとは何かを調べると SwiftUIの構造体であることがわかりました だから 末尾のクロージャ引数は このイニシャライザに渡されます その構造体に さて パラメータを見ると クロージャーの渡された先は そこにはViewBuilder 属性が含まれたことがわかります その属性はコンパイラにすべきことを伝えます ViewBuilderという 名前のリザルトビルダーを適用します クロージャに しかし ViewBuilderとは何ですか? さて その名前でタイプを探すと このタイプもSwiftUIにあると分かります @resultBuilder属性 があるかに注意してください それがリザルトビルダーであることを コンパイラーに知らせます Swiftがリザルトビルダータイプ を見つけたので クロージャーに適用を開始します 最初に行うことは変数を作成することです リザルトを生成するすべての ステートメントのために これらの変数が作成されると ViewBuilderでbuild Block方法にコールを書きます そして それらすべての変数をそれに渡します buildBlockの仕事は 処理または結合することです そのすべてのパラメータを単一の値に それが返すもの 次に コンパイラはreturn ステートメントを記述します クロージャからbuild Blockのリザルトを返す つまり 一般に コンパイラはコードを取得し コードを黄色で追加してViewBuilderは 作成したすべての値を1つの値に まとめることができる そのVStackがその内容として使用する 今は 修飾子型の方法がどのように 機能するかを説明します これに合う 修飾子型の方法は 自身変更の コピーか 次のものを返します または別のタイプに包まれた自己のコピー アクションを追加する そしてそれは同じステートメント 内でこれを行います それはまず自己を創造した したがって 値を変更することになります リザルトビルダーがそれを見る前に そして 他の修飾子型の方法 を呼び出すことができます そのメソッドのリザルトに いくつかの変更を適用して それらを一緒に構成できます リザルトビルダーがすべての値を確認する前に これらの2つのこと-- 修飾子を作成する機能と次の事実 リザルトビルダーがそれを見る 前に値を変更する-- SwiftDSLがしばしば修飾子 を使用する原因になります この2つはうまく連携します さて 心配していたことが1つあります リザルトビルダーを設計したときは Swiftの動作を大幅に変更させる場合は クライアントがDSL内の何も信頼できない 通常のSwiftコードのように機能したこと したがって リザルトビルダーを設計した時 バランスをとろうとした 有用なDSLを作成するのに 十分な能力を持っている間 Swiftの機能を確認して クライアントが期待するとおりに機能する リザルトビルダーは根本的に再解釈しません クライアントが書き込むコード ステートメントはまだ改行で終わり 呼び出しはまだ括弧を使用し 中括弧はまだ一致している必要があります すべてのSwift構文の基本 クライアントが期待するとおりに機能します また 表示されない新しい名前は導入されません 同じ場所に書かれた通常のコードから あまり意味のない言語機能がいくつかあります リザルトビルダーを使用している場合-- 主にキャッチやブレイクのようなもの 制御フローを実際にはうまく 適合しない方法で中断する ステートメントのリザルトをキャプチャ して使用するというアイデアに リザルトビルダーを使用している場合 これらの機能は無効になります 且つ いくつかの機能がある- if switch for-in ステートメントのように-- リザルトビルダー次のこと でない限り無効になります それらを実装するために使用される 追加のメソッドを提供する ただし Swiftでキーワードの 使用が許可された場合は それが通常どおりに機能します if-elseステートメントの ように終わることはありません trueブロックとfalse ブロックの両方を実行します または 一部の要素をスキップするループ リザルトビルダーは ステートメント のリザルトだけをキャプチャします そうでなければ捨てられていた 以外は何もありません ですから クライアントはそれら を信頼することができます
では これで リザルトビルダーが何であるか 及びそれらがどのように機能するかをわかりました それらを使用するDSLの設計を開始できます これまで言語に取り組んだことがない場合は ユーザはその考えが恐ろしいと 思うかもしれませんが しかし SwiftDSLの設計は 実際には SwiftAPIの 設計によく似ています Swift APIのように Swift DSLはゼロから始めていません それはSwiftの構文と機能を使用します アイデアや行動を表現する 解決しようとする問題に関連している DSLは単に追加機能だけを使用しています APIが常にそうしない Swift APIのように Swift DSLを 設計することができます すべてが問題を解決するいくつかの異なる方法で だからユーザの仕事は代替案を考え そして 最も良いと思うものを選択してください DSLに 潜在的なソリューション の大きなスペースがあります Swift APIのように SwiftDSLの最良の経験則 通常はデザインを選択することです 最も明確な使用サイトが得らる DSLは クライアントが投資する ことを前提としています 少し前もって言語を学ぶ そのため それは明瞭であることを優先しません 今まで見たことがない人に対して したがって 以前にAPIを設計 したことがある場合は DSLを設計するための良い起点があります そのことについてはいくつかの提案 及びDSLに使用するテクニックは API設計に非常にうまく移行できます この講演では App FrutaのDSLを設計します これの実用的な実装が見つかります Frutaサンプルコードで Frutaのソースコードには15 のスムージーレシピが含まれます DSLの前に 私たちは単に 各スムージーを作成しました メンバーワイズ初期化子を呼び出すことによって そしてそれを静的定数に割り当てます そして すべてのスムージーの配列を保存しました 別の静的定数に 特定のビューが次に応じて App内の購入が必要なレシピを含めたいかどうか リスト全体を返すか支給したものを除外します 今 これは完全に出荷できます そして ユーザが望んだら それに固執することができます しかし スムージーのレシピは かなり頻繁に更新されます Appの他の部分とは異なり それがデザイナーやマーケティング 担当者 及びマネージャーによって 更新されたので DSLが必要かもしれません それを少し簡単にするために そして 今それをしている方法を見て いくつかの欠点にきづいたが対策はありません 支給したスムージーをリストから除外する必要性 このコードをゆがめました allSmoothiesと hasFreeRecipe この関数でのみ使用されます それ以外の場合は存在する必要はありません しかし それらなしでこれを実装 ることを想像しようとすると なぜそうしなかったのかがわかります 配列作成の仕組み それに要素を追加し スムージーの実際のリストを秘めて始める この関数のポイントの一種である 同様に スムージーのリストが スムージーの定義とは別ですが少しばかげています これらの定数のいくつかは プレビューで使用されますが しかし それらのほとんどは このリストにのみ表示されます ユーザが一箇所でスムージーを定義するので 別のリストに追加し それは間違いの機会を生み出します 新しいスムージー定数を宣言するとどうなりますか しかし それをリストに追加するのを忘れましたか または スムージーを2回 追加するとどうなりますか 定義を顧みると 個々のスムージーの 私はまた 私を悩ませている 他の2つのことを見ました 1つは 成分リストが非常に言葉が多いことです 同様に 各エントリが繰り返されます 「測定」という言葉のいくつかのバージョンを3回 この行では気にかけている実際の情報は 1.5カップのオレンジです 行の残り部分に 有用なことは何も言っていません 見た目がすっきりしています 必然的にいくつかのサポート構文があります 重要な情報の周り でもこれだけあると周りの定型文は 私たちが伝えようとしている 情報を圧倒するだけです 私が気付くもう一つのことは行数の多さです 比較する各スムージーに専念 実際に存在する情報の量 ここで問題だとなることは 引数の長さの違いです これらの引数のいくつかは非常に短いです 単一の行に合わせることができます 他のものはより長く 本当に彼ら 自身の行を必要とします 今は短い引数を組み合わせることができます 一行に 次に 長い1つに別々の行を使用します しかし ほとんどのスタイルガイド はそれに眉をひそめています 構文が必要です 異なるスタイルにするのは一般の方法です それらをまとめるとたくさんの目標があります DSLで実現したいこと スムージーリストの管理を容易にします さて 次にやるべきことは さまざまな方法を見ることです DSLを設計して これらの目標を 達成することができます たくさんの異なる設計があります それはこれらの各ポイントに対処します やろうと思ったことを簡単に説明します 最初の3つの目標については そして 最後の目標をより詳細に調査できます すべての方法でスムージーリストを 定義することにしました スムージーは本体内で直接定義されます 静的変数を使用せずに 誰かがスムージーを定義することを 心配する必要がないから それをリストするのを忘れましょう 「SmoothieArrayBuilder」という リザルトビルダーを使用します DSLをアクティブ化する スムージーを配列に集めます そうすれば 配列リテラルを 使用する必要はありません または 物事を一時変数に収集します そして ifステートメントにスム ージーを入れることができます そのため 以前のようにリスト を濾過する必要はありません クライアントがすでにSwiftを 知っているので これは素敵です ifステートメントがどのように 使用されるかを知っています クライアントはおそらく理解できないでしょう "if includingPaid"は ほとんど問題ありません 材料の量を指定することにしました 修飾子型のメソッドを使用して 材料には「測定(方法 :)」と 呼ばれる方法があります 単位を取り 測定された成分を返す その単位の1つで その単位の量を変えたい場合は 測定された成分のスケール(方法:) 修飾子 数量とともに返す 渡した数を掛けます つまり 1カップのオレンジは1.5 カップのオレンジになります 1カップのアボカドは0.2 カップのアボカドになります さて なぜ異なる修飾子でスケールしますか? Frutaの画面の1つには コントロールがあります 材料の量を計測するために使用できる スムージーレシピで 以前に乗数を渡しました その量を掛けた各成分行に でも実際に次の方法を使えることに気づきました 成分を量るの代わりにスケール(方法:) 修飾子 行に渡される前に これにより 行ビューを簡略化できる スムージーのDSLデザインを少し調整することで その一部を再利用することができました プロジェクトの別の部分で したがって 最初の3つの 目標を達成するための変更により 新しいDSLが具体化し始めている ことが分かります それでは 最後の目標に焦点を当てましょう 個々のスムージーエントリーの再設計 よりコンパクトになりうまくいけば クライアントがつまずく可能性 のある 混乱の少ない句読点 いくつかの異なる方法を見てみましょう それを助けるためにこの情報を 手配することができます できることの1つは 修飾子型の 方法を使用することです 説明と材料を追加するように これは機能しますがそれは一種の言葉のみです 且つ 誰かが説明を忘れがちか または何かを2回指定することが発生します できるもう1つのことは 各フィー ルドにマーカータイプを与えます そしてそれらをリザルトビルダーの クロージャーに入れます しかし これはIDとタイトルを 独自の行に配置します これは避けようとすることです だから多分IDとタイトルを 移動することができます パラメータリストに戻り 他の2つのフィールドには マーカータイプを使用します でもこれはまだもう少し幸せだと思います 私たちが本当に必要とするよりも レシピのユーザーインターフェース を見て気づきました それらは常に特定の順序で表示されます タイトルを上部に説明を中央にいれて 成分のリストは下部にあります また タイトルや説明にわざわざ ラベルを付ける必要はありません 私たちは彼らの視覚的な階層を 許可する--事実はタイトルが 説明よりも目立つように表示されます -- 話をします だから私はそれからいくつかの インスピレーションを引き出して スムージーDSLも同じにする 必要があることと決めました タイトルを一番上に説明を中央に置き 材料のリストは下部にあります それは説明が下にあるという事実を可能にします タイトルよりもインデントされています -- そのため 視覚的に目立たなくなります-- 説明文字列の意味を伝える したがって ラベルを付ける必要はありません リザルトはすぐに理解できると思います 不必要な合併症なしで そして それをDSL全体の コンテキストに入れると かなり快適なフィット感があると思います しかし 同意しないかもしれません が それは大丈夫です DSLはプログラミング言語です 個人の趣味と主観的なトレードオフは プログラミング言語の設計の大きな部分です これは厳密にすべきではない という意味ではありません ユーザは自分の欲しいものの 明確な考えから始めるべきです 言語から 既存のソリューションがあるかどう かを調べる必要があります -- ifステートメントのようにそれは ユーザの問題を解決できます 理由はおなじみのソリューションを採用できれば 人々は新しいものを学ぶ必要はありません 言語の各部分がどのように考えられるべきか それの残りの部分と交流します Swift DSLでは それは次 のことを考えるべきと意味します DSLがその周りのSwift コードでどのように交流するか スケール(方法:)修飾子を選択したときのように 他の場所で使えるから 間違いを犯す対策を探す必要があります 両方もコンパイル時に検出可能 または書くことは全然できでません リコールすれば そのため 説明を修飾子にしませんでした それが誤りに無視された恐れがあります 以上の考えに基づいて いくつかの異なる可能性を考え出す必要があります それぞれがどのように使用されるか を想像してみてください 小さなモックアップを書きます そして お互いに対してそれらを比較検討します でも 結局は 通常 正しいものは明らかに見つかりません しかし 他のオプションが 明らかに間違っている場合 できることは何かを選ぶことだけです ユーザは言語のクライアント にとって最適だと感じます どちらが最適かわからない場合は ユーザはおそらく最も読みやすい方を好むべきです それでもわからない場合は... まあ 個人的には もっと大胆 なオプションを取るのが好きです 何かを試して うまくいかない 場合は元に戻したいです 一度も試してみて疑問と思うよりも 今はDSLがどのようになるかを決定したので さあ 続いて Frutaに追加しましょう 以前のスムージーの定義が置き換えられました DSLを使用する最後のすべての方法で しかし 私はまだDSLを実際に実装していません 当然のことながら 大量のエラーが発生しました しかし それは大丈夫です これを処理する場合には これらのエラーが私を導きます 私が解決する必要のある問題にそして最後には エラーなしで構築できるものがあります それでは 関数の先頭から始めましょう この最初のエラーを持つ: "Unknown attribute ‘SmoothieArrayBuilder’." リザルトビルダーは実際にはまだ存在しませんが もちろん それは機能しないと思います では これを直しましょう 「SmoothieArrayBuilder」というタイプ を作成することから始めます リザルトビルダーの属性でマークされます 現在 Swiftがこんなインスタ ンスを作成することはありません これは 単なる一連の静的 メソッドのコンテナのみです だから私はそれを列挙型にして 且つ どんな場合も定義しません 列挙型のインスタンスを作成することは不可能です ケースがない したがって これは人々がそれを 誤って使用することを防ぎます これだけを作成するとエラーが発生します リザルトビルダーにはbuildBlock メソッドが必要だと想定します それは1つを挿入する修正があるので だから私はその修正を受け入れます 次に それを実装する方法を取得します さて 以前からリコールすれば buildBlock機能の方法 は このコードがある場合です 個々のステートメントのセットで これらの各ステートメントは 変数に割り当てられます 変数はbuildBlockにすべて渡されます およびbuildBlockによって返される値 クロージャによって返されます したがって buildBlock メソッドの理由となります 多くのスムージーをパラメーター として受け入れる必要があります そして スムージーの配列を返します 可変個引数のパラメータを使用して実装する場合 任意数量のスムージーでも メソッドに渡すことができます...
...及びビルド... ...まあ 得るものは少し良いです まだたくさんのエラーがあります しかし SmoothieArrayBuilder というものは 無効な属性がなくなりました 且つ 属性も色を変えました 既知のタイプであることを示すために それでは 次のエラーに移りましょう スムージーのイニシャライザー用のもの ある人は 私たちがトレーリング クロージャを渡すと言います 文字列パラメータに もう一人は measuredIngredients引数が 欠落していると言います つまり 明らかに古い初期化子を使用しています これは 説明と成分をパラメーター として期待しています 新しいイニシャライザを作る必要があります それでは そのイニシャライザを実装しましょう ID タイトル および末尾のクロージャをつく 説明と材料を返す 今からあなたにお伝えましょう 後でこの初期化子に戻る必要があります 今ビルドすると すべてのエラーがクリアされます スムージーの初期化子から でも これは完全に機能していると 思うかもしれませんが しかし それは実際には少し誤解を 招く恐れがあります ほら ここに別のエラーがあります 次の原因で引き起こされたifステートメントで SmoothieArrayBuilderが 終了していません そのエラーがそこにあるので Swiftはまだクロージャーの 内部をチェックしていません 例えば このクロージャに入って ただ書くなら 私が知っているいくつかの確率 変数名は存在しません 後でそれを構築すると Swift .はエラーにフラグを立てません 起こっていることは Swiftが 次のことを見るということです 適切に適用しなかった ので 実際には信頼していません これらのクロージャで見つかったエラー 実際には正確でしょう したがって まだエラーを探していません 後で SmoothieArrayBuilderを 終了すると 突然これらのエラーが表示されるようになります その時点で修正できます しかし今のところ作業を続ける方は簡単です SmoothieArrayBuilderで だから それらのクロージャを 脇に置いておきましょう そして 次のエラーに進みます このエラーを見ると Swiftは 次のように記述しています: SmoothieArrayBuilderでifステートメントを 使用することはできません しかし それをサポートするために 追加できる方法があります ifステートメントは この数少 ないSwift機能の1つです リザルトビルダーが次を実行する限り それらは無効になります それらをサポートするための 追加のメソッドを実装します したがって これの実装を開始するには 修正を押して 何が追加されるかを見てみましょう したがって どうやらメソッドを 実装する必要があります buildOptionalと呼ばれる オプションのスムージーの配列を取る スムージーの配列を返します では この方法はどのように使用されますか さて allメソッドのこの 単純化された例を見てください これには else文が付かない のifステートメントがあります ifステートメントのない前の例のように これにより 各ステートメント のリザルトがキャプチャされます 変数に それらの変数を buildBlockに渡します クロージャーからbuild Blockのリザルトを返します 唯一の問題は キャプチャされたらどうなります ifステートメントのリザルトはなんでしょうか さて それが最初にやろうとすることは ifステートメントの本文にあるすべての ステートメントをキャプチャします 変数に 次に buildBlockを使用 してこれらの変数を結合します トップレベルと同じように しかし これは buildOptionalの出所です その内部buildBlock 呼び出しのリザルトを返す代わりに Swiftはそれを buildOptionalに渡します およびbuildOptional によって返される値は 全体としてifステートメントの値になります しかし これは変数を初期化しないままにします if条件がfalseの場合 そのため build Optionalのパラメータ スムージーのオプションの配列です Swiftはelseブランチを追加します ifステートメントのリザルトの値を設定します buildOptionalnilからの戻り値に SmoothieArrayBuilderの場合 これのリザルトは次のとおりです buildOptionalがどちらかを 返すようにします buildBlockから渡された配列 または パラメータがnilの場合は 空の配列を返します 今それを構築すると次のようになります... ...本当に奇妙に見えるエラー スムージー型の配列を可変個引数 として渡すことはできませんか? 何? さて 生成されたコードに戻りましょう ifステートメントは 最終的に スムージーの配列を生成します しかし実際には buildBlockはスムージー の配列を必要としません 単一のスムージーが必要です それを変更する必要があります だから多分私たちはbuildBlockを 作ることができます スムージーの配列を引数として取り 次に flatMapを使用してこれらを一緒に連結します スムージーのそれらの多くの配列を スムージーの単一の配列に よし それを構築し そして... ...いいえ これで ifステートメントが機能しますが しかし すべてのスムージーラインが壊れました それらが必要です スムージー型の値をスムージーの配列に変換できません どうしたんですか さて buildBlockが変更されたので スムージーの配列と一致するように buildOptionalによって返されます しかし それもそれぞれのと一致する 必要があることを忘れました 通常のステートメントによって返されるスムージー おっとっと 基本的に これを許可する場合 任意の高度な制御フロー buildBlockのリターン タイプは次のような何かである buildBlockにパラメーター として渡される これを実現するには2つの方法があります 1つの方法は build Blockを確認することです および他のリザルトビルダー メソッドは型を返します リザルトビルダーで許可されたステート メントと互換性があります たとえば これがSwiftUIのViewBuilder の仕組みです SwiftUI DSLでは すべてがViewプロトコルに準拠しています buildBlockによって 返されるタイプを含む およびその他のビュービルダーメソッド しかし それは私たちのスムージー DSLにはあまり適していません SwiftUIビューとは異なり 他のスムージーの中にスムージーを 入れ子にすることはありません できる他のことはリザルトビルダーを 次のことさせます 通常のステートメントの値を変換します buildBlockによって返される同じ型に これは このDSLにより適合しています これは 「buildExpression」というメソッド を追加することで実現できます buildExpression メソッドを追加すると Swiftは各裸の式をそのメソッドに渡します 変数に取り込む前に それはそれらを配列に変換する 機会を与えてくれます しかし 他のリザルトビルダーメソッドからの値は buildOptional及びbuildBlock によって生成されたもののように これらの呼び出しに巻き込まれないように そのため この変換はそれらに 適用されていません -- すでに配列を返しているのでこれは良いことです だから やろうとしていることは buildExpression メソッドを実装します Xcodeのコード補完 リザルトビルダーメソッドについて すべて知っているため 署名を書いてもらうことができます 次に パラメータタイプをスムージーに変更し 式パラメータだけを返します 配列リテラルに巻き込まれている だから今 単一のスムージーは このものに変わります buildBlockが必要と するスムージーの配列 それを構築し そして... ...素晴らしいですね ifステートメントは機能し スムージーの初期化子も機能します ただし ミニマップを見ると ここに2番目のifステートメント があることがわかります それは機能しません これはelse文があるためです buildOptionalは 実際には普通のif ステートメントにのみ機能します elseステートメント else-if または switch がある場合は 一対のメソッドを実装する必要があります buildEither 1と呼ばれ 及びbuildEither 2と呼ばれます fix-itを使用してこれらを作成し 次に それらがどのように機能 するかについて話します それでは この単純化された例を見てみましょう if-elseステートメントを使用します ほとんどの変換は buildO ptionalの変換と同じです buildOptionalと同様に if-elseステートメント全体は 最後に単一の変数を埋めることになります また buildOptionalのように ifステートメントの各ブロック その中のステートメントが変数に キャプチャされます その後 buildBlockが使用されます それらを1つの値に結合します 普通のifステートメントとの 違いは次のとおりです buildOptionalを使用 して最終値を生成するの代わりに buildEitherメソッドの 1つを使用します ifとelseのように2つのブランチがある場合 次に 最初にbuildEitherを使用します 2番目はbuildEitherを使用します これにより 気になるリザルト ビルダーが可能になります それらを区別するためにどのブランチを使用するか さて 私たちが何をしているのか 疑問に思っているなら 3つ以上のケースがある場合 その答えは実際にはかなりクールです 平衡の二進の木を構築します 各枝を1つの葉として 次に 非リーフノードを必要な 呼び出しとして扱います エッジで最初のbuildEitherを使用する かどうかを教えてくれます または二番目のbuildEither 各ブランチが使用する呼び出し のシーケンスに注意します 次に 変数に割り当てるコードを生成します その一連の呼び出しで したがって 2つの方法しかありませんが リザルトビルダーはまだ区別できます 3つのブランチの間 悪くないです とにかく buildEitherメソッドがどのよう に機能するかがわかったので 先に進んでそれらを書くことができます SmoothieArrayBuilder は実際には気にしないので どのブランチを利用したか 私たちが する必要のあることはありません 配列引数を返すだけです
だから今 私たちはこれを構築し そして... それでもまだ機能しません しかし 成功は近くなります この種のエラーを覚えているかもしれません スムージーの配列の問題が発生したときから 今だけ スムージータイプに ついて文句を言っていません これは「()」タイプです それは空のタプルで これはおそらくボイドと認められるタイプでしょう 生成されたコードについて考えると これが問題である理由は明らかになります buildExpressionと呼ばれますが しかし 渡された式は logger.logを呼び出しています これはスムージーではなくボイドを返します では buildExpression のオーバーロードを記述します これはVoidパラメータを取り 空の配列を返します 次に それを再構築して これで ログの呼び出しが正しく 機能するようになりました 数え切れないほどのエラーがありますが しかし これは実際には良いニュースです ほら これらのエラーの最初のもの はその偽の変数からのものです 私はそれを最初に追加して Swiftがエラーを検出し ていなかったことを示します 後続のクロージャで 今では それは私たちが終了したことを意味します スムージーの配列ビルダー! はい エラーが発生しました その偽の変数を削除して 残った ものを見てみましょう よく見ると 次の問題が見つかれます ここでは 見た目よりもやることが 少なくなっています すべての説明行に同じ警告がありますが そして すべての成分行に同じ のエラーが2つあります ですから 100個のエラーだけでなく 何十個のダースの警告もある場合 これは実際には2つの同じ問題です 何度も起こっていた エラーを詳しく見てみましょう 1つはこれらの成分行にあります コンパイラには2つの不満があります どのタイプのカップを探すべきかわからなくて 成分に「測定済み」という メンバーがいるとは思われません まあ それは分かりやすいで実装していません 測定された(ツール :)また はスケール(方法:) 修飾子 そのため 「測定済み」と呼ばれる ものは見つかりません 且つ それはカップが何であるかをわかりません 測定されたもの(方法:)がわからないため ボリューム単位を取ることに想定します それでは MeasuredIngredient.swift にアクセスして これらの2つの修飾子を実装しましょう 測定値(方法:)は成分になり MeasuredIngredientを返します 発信者が渡したユニットの1つで スケール(方法:)はMeasured Ingredientに進みます 新しいMeasuredIngredient を返します 測定値に発信者が渡した係数を掛けたもの Smoothie.swift に戻ってビルドします... ...そして 確定します より多くの警告が表示されエラーはわずかです よく見ると 1種類の警告しか表示されていません -- クロージャーの各式は 無視されたことがわかりました-- ある種のエラー -- クロージャにはreturnステー トメントがないことを示します 原因を究明するには これらのトレーリングのクロージャ について説明しましょう リザルトビルダーがそれらとどのように 相互作用するか この例では SmoothieArrayBuilderが 外側のステートメントに影響を与えます 以前に見た方法と同じです それらはbuildExpressionに 渡されます 変数に保存され 変数はbuildBlockに渡されます しかし これらのクロージャはどうですか? リザルトビルダーはそれらに何をしますか まあ それは... ...まったく何もありませんクロージャは 実際には 関数内にネストされた独立の関数です リザルトビルダーが適用された先 リザルトビルダーは1つの関数にのみ適用されます 内部にネストされた関数や クロージャには影響しません リザルトビルダーの影響を受けたい場合は 次に 他の方法でそれらに適用する必要があります リザルトビルダーを適用する方法は3つあります 関数本体に まずは属性を書きます 関数またはプロパティで直接で SmoothieArrayBuilder の場合と同じように リザルトビルダーを適用する2番目の方法は 関数に書き込むことです またはプロトコル内のプロパティ要件 次に それは自動的に適用されます すべての適合タイプの実装に SwiftUIビューの本体プロ パティは次のように機能します ViewBuilder属性が 次のものに適用されます ビューのボディ要件に そのため ビューの本体のプロパ ティにも自動的に適用されます リザルトビルダーを適用する3番目の方法は それをクロージャのパラメータ の前に書き込むことです そうすると Swiftはこれを推測します そのパラメータに渡された任意のクロージャは リザルトビルダーをそれに適用する必要があります Swiftがリザルトビルダーを推測した場合 プロトコルまたはパラメータから 実際に適用したくなければ 明示的に値を返すことで無効にできます returnステートメントを使用して ただし この場合 クロージャを使用しているため これら3つのオプションの最後が必要です クロージャパラメータから リザルトビルダーを推測します 属性を書くことでそれを行います 引数ラベルの前で 今では SmoothieArrayBuilderを ここに書くことができます しかし それはおそらくこれを行う ための最適の方法ではありません SmoothieArrayBuilderは スムージーの 配列を生成しますが しかし このクロージャーがスムー ジーを生み出す必要がありません 文字列と成分の配列を生成する必要があります しかし ifステートメントは必要ありません または このクロージャで無効な 呼び出しを返す必要もありません 本当に 私たちは独立のセットを適用します 言語規則の(独立のセット)をこのクロージャに 2番目のルールセットを混在させるのではなく SmoothieArrayBuilderに 新しいリザルトビルダーを作成する方 がいいと思います これはこれらの新しいルールを実装します それをSmoothieBuilderと呼び 新しいタイプを作成しましょう そして buildBlock メソッドの作成を開始します さて これは少し特別です MeasuredIngredientsを いくつでも受け入れたいのですが しかし 私たちはまた 前に文字列を取りたいです では どのようにそれを行いますか? さて SmoothieBuilder の方法を考えると-- これは 単純なリザルトビルダーである ことを忘れないでください buildBlockメソッドのみで -- 拡大される予定で それらの各行が渡されます 異なるパラメータとして だから多分ただ文字列パラメータ だけを書くことができるようです buildBlockの開始時に 次に 最初のステートメントは この文を生成する必要があります MeasuredIngredient の代わりに文字列 それでは それを試してみましょう 前に文字列パラメータを追加し 文字列のタプルを返すようにします と材料の配列
私たちがこれを構築すれば... ねえ それを見てくださいゼロのエラー! DSLは機能します 現在 リザルトビルダーはさらにいくつ かの機能をサポートしています for-inループのように 最後の戻りリザルトを処理します それらを使用したい場合は それらはSwiftプログラミング 言語の本で説明されています でも終わる前に注意を呼びたい 言語設計の最も重要な部分の1つに: 良いエラーメッセージ 言語を設計するときに学ぶことの1つは 無効なコードを書く方法は他にも たくさんあるということです 有効なコードよりも しばらく 時間を費やす必要があります 無効なコードに発生するエラーについて考えます コードが間違っている時にあなたの行動は コードが正しい場合と同じくらい重要です さて Swift DSLの場合 Swiftのエラー処理を無料で利用できます しかし クライアントが受け取る エラーメッセージは 一般的なSwiftコード用に設計されます 彼らはユーザの言語の規則の 観点から表現されていません でも クライアントに問題を明確に 伝えられない可能性があります たとえば 誰かが忘れたと想像してください これらのスムージーの1つに説明を入れます Swiftはエラーメッセージを出力します しかし これは少し不明確です それは最初の成分が 文字列に変換することはできない ことに文句を言ます では このコードはどのようにこの エラーを生成するのでしょうか? しかし Swiftコンパイラ は本当に理解していません 私たちのスムージーDSLのセマンティクス Swiftコードのセマンティクス のみを理解します リザルトビルダーを使用するために生成されます したがって このエラーを診断しようとすると この値を説明とは見なしません スムージーまたは最初の成分の それはbuildBlockへの 最初の引数と考えています v0 buildBlockの最初の引数は MeasuredIngredientであり しかし それは文字列パラメータに渡されます したがって Swiftはこの エラーを次のように考えています 「MeasuredIngredient を渡しようとしていますが 文字列パラメータに しかし MeasuredIngredient を文字列に変換できません」 エラーメッセージは技術的に間違っていませんが でも それもあまり役に立ちません コンパイラエンジニアには このための秘訣があります コンパイラに無効なものをサポートさせます ただし 実行するとエラーが発生します たとえば Swiftの関数文法に スロットがあります ここでは スロー リスロー また は何も書書かないことができます サポートされていない他の単語を書く場合は コンパイラは それが想定されていたと推測します 別のステートメントの一部であり エラーが発生します セミコロンを追加するか 改行を 使用するように指示します しかし 具体的に「試してみる」と書くと 別のエラーが発生します コンパイラはそれをthrowsに 置き換えることを提案します 次に ファイルの残りの部分を解析します 代わりにそこにスローを書いたかのように これは Swiftパーサーに追加 した特殊なケースです 開発者が時々気づいた ここに他のエラー処理キーワードを入力することに 彼らがスローを書くことを意味するとき そのため 文書化されていない 小さな拡張機能を作成しました 言語の形式文法に ここでそれらの誤ったキーワードを解析します 次に 通常とは異なるエラーを診断します その特定の間違いに合わせて調整 似たようなことをすることができる ので 私はこれを指摘します リザルトビルダーで エラー動作を改善します 具体的には 過負荷になった場合 悪いコードに一致するリザルトビルダーメソッドの 次に その過負荷を使用不可としてマークします 診断時に使用するエラーメッセージを指定できます したがって 一般的なエラーが発生する代わりに それは問題をうまく伝えないかもしれない クライアントはより具体的な エラーメッセージを受け取ります その間違いに合わせて やりたいことはbuild Blockをコピーします ブロックを照合するために 説明パラメータを削除します そこに材料リストだけで 本文をfatalError()に置き換えます したがって 戻り値を偽造する必要はありません このメソッドが正常に呼び出される ことはありません だから本文はただ有効なもの 次に このオーバーロードを 使用不可としてマークします 問題をより明確に説明するメッセージを伝えます この利用できない注釈はメソッドが 実際には使用できません それに呼び出しを書くならばそれはエラーです だから今 私が一番上に戻って再構築すると はるかに明確な説明が得られることがわかります 間違いこと 最初の材料は文字列でなければ ならないと言う代わりに 説明文字列が欠落していることを示しています そのため クライアントはこのことを開始しません 材料が間違っていると思う または 文字列が何のためにある のか疑問に思う必要があります エラーは彼らにそのことを真っ先に伝えます それははるかに良い経験です 且つ 覚えておくべき最も重要なことは DSLの実装について クライアントの体験を向上させる ことがすべてだということです DSLは 複雑で反復的なコードを 作成する可能性があります クライアントが物事を定義できるよ うして はるかに明確になります メカニズムを心配せずに 定義を組み立てる リザルトビルダーは強力なツールです DSLが定義されている値を収集できる そして修飾子スタイルのメソッド それらの値を変更するための 構成可能な方法を提供します リザルトビルダーがそれらをキャプチャする前に ただし DSLを作成する場合は クライアントはそれを使用する方法 を学ぶ必要があります 価値がある場合にのみDSLを提供する 彼らの時間と労力 お時間をいただきありがとうございました いくつかの小さな言語の構築を楽しみにしています ♪
-
-
3:15 - FavoriteSmoothies view
struct FavoriteSmoothies: View { @EnvironmentObject private var model: FrutaModel var body: some View { SmoothieList(smoothies: model.favoriteSmoothies) .overlay( Group { if model.favoriteSmoothies.isEmpty { Text("Add some smoothies!") .foregroundColor(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity) } } ) .navigationTitle("Favorites") } }
-
3:38 - FavoriteSmoothies view (hypothetical alternative)
// Hypothetical code--not actually supported by SwiftUI struct FavoriteSmoothies: View { @EnvironmentObject private var model: FrutaModel var body: some View { var list = SmoothieList(smoothies: model.favoriteSmoothies) let overlay: View if model.favoriteSmoothies.isEmpty { var text = Text("Add some smoothies!") text.foregroundColor = .secondary var frame = Frame(subview: text) frame.maxWidth = .infinity frame.maxHeight = .infinity overlay = frame } else { overlay = EmptyView() } list.addOverlay(overlay) list.navigationTitle = "Favorites" return list } }
-
3:59 - FavoriteSmoothies view
struct FavoriteSmoothies: View { @EnvironmentObject private var model: FrutaModel var body: some View { SmoothieList(smoothies: model.favoriteSmoothies) .overlay( Group { if model.favoriteSmoothies.isEmpty { Text("Add some smoothies!") .foregroundColor(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity) } } ) .navigationTitle("Favorites") } }
-
6:17 - FavoriteSmoothies view
struct FavoriteSmoothies: View { @EnvironmentObject private var model: FrutaModel var body: some View { SmoothieList(smoothies: model.favoriteSmoothies) .overlay( Group { if model.favoriteSmoothies.isEmpty { Text("Add some smoothies!") .foregroundColor(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity) } } ) .navigationTitle("Favorites") } }
-
9:26 - Simple result builder example
VStack { Text("Title").font(.title) Text("Contents") }
-
9:36 - Simple result builder example + struct VStack
VStack { Text("Title").font(.title) Text("Contents") } struct VStack<Content: View>: View { … init(@ViewBuilder content: () -> Content) { self.content = content() } }
-
9:40 - Simple result builder example + struct VStack + trailing closure applied
VStack /* .init(content: */ { Text("Title").font(.title) Text("Contents") } /* ) */ struct VStack<Content: View>: View { … init(@ViewBuilder content: () -> Content) { self.content = content() } }
-
9:50 - Simple result builder example + struct VStack + trailing closure applied + enum ViewBuilder
VStack /* .init(content: */ { Text("Title").font(.title) Text("Contents") /* return // TODO: build results using ‘ViewBuilder’ */ } /* ) */ struct VStack<Content: View>: View { … init(@ViewBuilder content: () -> Content) { self.content = content() } } @resultBuilder enum ViewBuilder { static func buildBlock(_: View...) -> some View { … } }
-
VStack /* .init(content: */ { /* let v0 = */ Text("Title").font(.title) /* let v1 = */ Text("Contents") /* return ViewBuilder.buildBlock(v0, v1) */ } /* ) */ struct VStack<Content: View>: View { … init(@ViewBuilder content: () -> Content) { self.content = content() } } @resultBuilder enum ViewBuilder { static func buildBlock(_: View...) -> some View { … } }
-
14:49 - Fruta's smoothie lists, pre-DSL
// Fruta’s Smoothie lists extension Smoothie { static let berryBlue = Smoothie( id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ MeasuredIngredient(.orange, measurement: Measurement(value: 1.5, unit: .cups)), MeasuredIngredient(.blueberry, measurement: Measurement(value: 1, unit: .cups)), MeasuredIngredient(.avocado, measurement: Measurement(value: 0.2, unit: .cups)) ], hasFreeRecipe: true ) static let carrotChops = Smoothie(…) static let crazyColada = Smoothie(…) // Plus 12 more… } extension Smoothie { private static let allSmoothies: [Smoothie] = [ .berryBlue, .carrotChops, .crazyColada, // Plus 12 more… ] static func all(includingPaid: Bool = true) -> [Smoothie] { if includingPaid { return allSmoothies } logger.log("Free smoothies only") return allSmoothies.filter { $0.hasFreeRecipe } } }
-
14:50 - Fruta's smoothie lists, pre-DSL (hypothetical alternative)
// Fruta’s Smoothie lists (hypothetical alternative) extension Smoothie { static let berryBlue = Smoothie( id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ MeasuredIngredient(.orange, measurement: Measurement(value: 1.5, unit: .cups)), MeasuredIngredient(.blueberry, measurement: Measurement(value: 1, unit: .cups)), MeasuredIngredient(.avocado, measurement: Measurement(value: 0.2, unit: .cups)) ], hasFreeRecipe: true ) static let carrotChops = Smoothie(…) static let crazyColada = Smoothie(…) // Plus 12 more… } extension Smoothie { static func all(includingPaid: Bool = true) -> [Smoothie] { var allSmoothies: [Smoothie] = [ .berryBlue, .carrotChops, ] if includingPaid { allSmoothies += [ .crazyColada, // Plus more ] } else { logger.log("Free smoothies only") } return allSmoothies } }
-
14:51 - Fruta's smoothie lists, pre-DSL
// Fruta’s Smoothie lists extension Smoothie { static let berryBlue = Smoothie( id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ MeasuredIngredient(.orange, measurement: Measurement(value: 1.5, unit: .cups)), MeasuredIngredient(.blueberry, measurement: Measurement(value: 1, unit: .cups)), MeasuredIngredient(.avocado, measurement: Measurement(value: 0.2, unit: .cups)) ], hasFreeRecipe: true ) static let carrotChops = Smoothie(…) static let crazyColada = Smoothie(…) // Plus 12 more… } extension Smoothie { private static let allSmoothies: [Smoothie] = [ .berryBlue, .carrotChops, .crazyColada, // Plus 12 more… ] static func all(includingPaid: Bool = true) -> [Smoothie] { if includingPaid { return allSmoothies } logger.log("Free smoothies only") return allSmoothies.filter { $0.hasFreeRecipe } } }
-
18:05 - Near-final DSL design
// DSL top-level design @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { Smoothie( // TODO: Change these parameters id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ Ingredient.orange.measured(with: .cups).scaled(by: 1.5), Ingredient.blueberry.measured(with: .cups), Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) ] ) Smoothie(…) if includingPaid { Smoothie(…) Smoothie(…) } else { logger.log("Free smoothies only") } }
-
19:57 - Possible DSL description/ingredient designs (start)
// Possible DSL description/ingredient designs Smoothie( id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ Ingredient.orange.measured(with: .cups).scaled(by: 1.5), Ingredient.blueberry.measured(with: .cups), Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) ] )
-
20:11 - Possible DSL description/ingredient designs (modifiers)
// Possible DSL description/ingredient designs Smoothie(id: "berry-blue", title: "Berry Blue") .description("Filling and refreshing, this smoothie will fill you with joy!") .ingredient(Ingredient.orange.measured(with: .cups).scaled(by: 1.5)) .ingredient(Ingredient.blueberry.measured(with: .cups)) .ingredient(Ingredient.avocado.measured(with: .cups).scaled(by: 0.2))
-
20:25 - Possible DSL description/ingredient designs (all marker types)
// Possible DSL description/ingredient designs Smoothie { ID("berry-blue") Title("Berry Blue") Description("Filling and refreshing, this smoothie will fill you with joy!") Recipe( Ingredient.orange.measured(with: .cups).scaled(by: 1.5), Ingredient.blueberry.measured(with: .cups), Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) ) }
-
20:36 - Possible DSL description/ingredient designs (some marker types)
// Possible DSL description/ingredient designs Smoothie(id: "berry-blue", title: "Berry Blue") { Description("Filling and refreshing, this smoothie will fill you with joy!") Recipe( Ingredient.orange.measured(with: .cups).scaled(by: 1.5), Ingredient.blueberry.measured(with: .cups), Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) ) }
-
21:13 - Possible DSL description/ingredient designs (no marker types)
// Possible DSL description/ingredient designs Smoothie(id: "berry-blue", title: "Berry Blue") { "Filling and refreshing, this smoothie will fill you with joy!" Ingredient.orange.measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry.measured(with: .cups) Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) }
-
21:43 - Final DSL design
// DSL top-level design @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { Smoothie(id: "berry-blue", title: "Berry Blue") { "Filling and refreshing, this smoothie will fill you with joy!" Ingredient.orange.measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry.measured(with: .cups) Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) } Smoothie(…) { … } if includingPaid { Smoothie(…) { … } } else { logger.log("Free smoothies only") } }
-
24:05 - Basic SmoothieArrayBuilder
@resultBuilder enum SmoothieArrayBuilder { static func buildBlock(_ components: Smoothie...) -> [Smoothie] { return components } }
-
24:39 - How ‘buildBlock(…)’ works
// How ‘buildBlock(…)’ works @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = */ Smoothie(id: "berry-blue", title: "Berry Blue") { … } /* let v1 = */ Smoothie(id: "carrot-chops", title: "Carrot Chops") { … } // …more smoothies… /* return SmoothieArrayBuilder.buildBlock(v0, v1, …) */ }
-
25:03 - Basic SmoothieArrayBuilder
@resultBuilder enum SmoothieArrayBuilder { static func buildBlock(_ components: Smoothie...) -> [Smoothie] { return components } }
-
25:56 - Smoothie initializer (incomplete)
extension Smoothie { init(id: Smoothie.ID, title: String, /* FIXME */ _ makeIngredients: () -> (String, [MeasuredIngredient])) { let (description, ingredients) = makeIngredients() self.init(id: id, title: title, description: description, measuredIngredients: ingredients) } }
-
27:47 - SmoothieArrayBuilder with simple ‘if’ statements (incorrect)
@resultBuilder enum SmoothieArrayBuilder { static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: Smoothie...) -> [Smoothie] { return components } }
-
28:01 - How ‘if’ statements work with ‘buildOptional(_:)’
// How ‘if’ statements work with ‘buildOptional(_:)’ @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = */ Smoothie(id: "berry-blue", …) { … } /* let v1 = */ Smoothie(id: "carrot-chops", …) { … } /* let v2: [Smoothie] */ if includingPaid { /* let v2_0 = */ Smoothie(id: "crazy-colada", …) { … } /* let v2_1 = */ Smoothie(id: "hulking-lemonade", …) { … } /* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1) v2 = SmoothieArrayBuilder.buildOptional(v2_block) */ } /* else { v2 = SmoothieArrayBuilder.buildOptional(nil) } */ /* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */ }
-
29:07 - SmoothieArrayBuilder with simple ‘if’ statements (incorrect)
@resultBuilder enum SmoothieArrayBuilder { static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: Smoothie...) -> [Smoothie] { return components } }
-
29:28 - Why didn’t our ‘buildOptional(_:)’ work?
// Why didn’t our ‘buildOptional(_:)’ work? @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = */ Smoothie(id: "berry-blue", …) { … } /* let v1 = */ Smoothie(id: "carrot-chops", …) { … } /* let v2: [Smoothie] */ if includingPaid { /* let v2_0 = */ Smoothie(id: "crazy-colada", …) { … } /* let v2_1 = */ Smoothie(id: "hulking-lemonade", …) { … } /* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1) v2 = SmoothieArrayBuilder.buildOptional(v2_block) */ } /* else { v2 = SmoothieArrayBuilder.buildOptional(nil) } */ /* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */ }
-
29:40 - SmoothieArrayBuilder with simple ‘if’ statements (still incorrect)
@resultBuilder enum SmoothieArrayBuilder { static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } }
-
30:14 - Why didn’t our ‘buildOptional(_:)’ work?
// Why didn’t our ‘buildOptional(_:)’ work? @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = */ Smoothie(id: "berry-blue", …) { … } /* let v1 = */ Smoothie(id: "carrot-chops", …) { … } /* let v2: [Smoothie] */ if includingPaid { /* let v2_0 = */ Smoothie(id: "crazy-colada", …) { … } /* let v2_1 = */ Smoothie(id: "hulking-lemonade", …) { … } /* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1) v2 = SmoothieArrayBuilder.buildOptional(v2_block) */ } /* else { v2 = SmoothieArrayBuilder.buildOptional(nil) } */ /* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */ }
-
31:23 - The ‘buildExpression(_:)’ method
// The ‘buildExpression(_:)’ method @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "berry-blue", …) { … } /* ) */ /* let v1 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "carrot-chops", …) { … } /* ) */ /* let v2: [Smoothie] */ if includingPaid { /* let v2_0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "crazy-colada", …) { … } /* ) */ /* let v2_1 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "hulking-lemonade", …) { … } /* ) */ /* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1) v2 = SmoothieArrayBuilder.buildOptional(v2_block) */ } /* else { v2 = SmoothieArrayBuilder.buildOptional(nil) } */ /* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */ }
-
31:44 - SmoothieArrayBuilder with simple ‘if’ statements (correct)
@resultBuilder enum SmoothieArrayBuilder { static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } static func buildExpression(_ expression: Smoothie) -> [Smoothie] { return [expression] } }
-
32:48 - SmoothieArrayBuilder with ‘if’-‘else’ statements
@resultBuilder enum SmoothieArrayBuilder { static func buildEither(first component: [Smoothie]) -> [Smoothie] { return component } static func buildEither(second component: [Smoothie]) -> [Smoothie] { return component } static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } static func buildExpression(_ expression: Smoothie) -> [Smoothie] { return [expression] } }
-
32:53 - How ‘if’-‘else’ statements work with ‘buildEither(…)’
// How ‘if’-‘else’ statements work with ‘buildEither(…)’ @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { /* let v0: [Smoothie] */ if includingPaid { /* let v0_0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { … } /* ) */ /* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0) v0 = SmoothieArrayBuilder.buildEither(first: v0_block) */ } else { /* let v0_0 = SmoothieArrayBuilder.buildExpression( */ logger.log("Only got free smoothies!") /* ) */ /* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0) v0 = SmoothieArrayBuilder.buildEither(second: v0_block) */ } /* return SmoothieArrayBuilder.buildBlock(v0) */ }
-
33:37 - How more complicated statements work with ‘buildEither(…)’
// How more complicated statements work with ‘buildEither(…)’ var v0: [Smoothie] switch userRegion { case .americas: // ...smoothies omitted... /* let v0_block = SmoothieArrayBuilder.buildBlock(...parameters omitted...) v0 = SmoothieArrayBuilder.buildEither(first: SmoothieArrayBuilder.buildEither(first: v0_block)) */ case .asiaPacific: // ...smoothies omitted... /* let v0_block = SmoothieArrayBuilder.buildBlock(…) v0 = SmoothieArrayBuilder.buildEither(first: SmoothieArrayBuilder.buildEither(second: v0_block)) */ case .eastAtlantic: // ...smoothies omitted... /* let v0_block = SmoothieArrayBuilder.buildBlock(…) v0 = SmoothieArrayBuilder.buildEither(second: v0_block) */ }
-
34:12 - SmoothieArrayBuilder with ‘if’-‘else’ statements
@resultBuilder enum SmoothieArrayBuilder { static func buildEither(first component: [Smoothie]) -> [Smoothie] { return component } static func buildEither(second component: [Smoothie]) -> [Smoothie] { return component } static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } static func buildExpression(_ expression: Smoothie) -> [Smoothie] { return [expression] } }
-
34:54 - How ‘if’-‘else’ statements work with ‘buildEither(…)’
// How ‘if’-‘else’ statements work with ‘buildEither(…)’ @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { /* let v0: [Smoothie] */ if includingPaid { /* let v0_0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { … } /* ) */ /* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0) v0 = SmoothieArrayBuilder.buildEither(first: v0_block) */ } else { /* let v0_0 = SmoothieArrayBuilder.buildExpression( */ logger.log("Only got free smoothies!") /* ) */ /* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0) v0 = SmoothieArrayBuilder.buildEither(second: v0_block) */ } /* return SmoothieArrayBuilder.buildBlock(v0) */ }
-
35:07 - SmoothieArrayBuilder with support for ‘Void’ results
@resultBuilder enum SmoothieArrayBuilder { static func buildEither(first component: [Smoothie]) -> [Smoothie] { return component } static func buildEither(second component: [Smoothie]) -> [Smoothie] { return component } static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } static func buildExpression(_ expression: Smoothie) -> [Smoothie] { return [expression] } static func buildExpression(_ expression: Void) -> [Smoothie] { return [] } }
-
36:41 - Modifier-style methods on Ingredient and MeasuredIngredient
extension Ingredient { func measured(with unit: UnitVolume) -> MeasuredIngredient { MeasuredIngredient(self, measurement: Measurement(value: 1, unit: unit)) } } extension MeasuredIngredient { func scaled(by scale: Double) -> MeasuredIngredient { return MeasuredIngredient(ingredient, measurement: measurement * scale) } }
-
37:32 - Closures and result builders
// Closures and result builders @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { /* let v0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { "Filling and refreshing, this smoothie will fill you with joy!" Ingredient.orange.measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry.measured(with: .cups) Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) } /* ) */ /* let v1 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { "Packed with vitamin A and C, Carrot Chops is a great way to start your day!" Ingredient.orange.measured(with: .cups).scaled(by: 1.5) Ingredient.carrot.measured(with: .cups).scaled(by: 0.5) Ingredient.mango.measured(with: .cups).scaled(by: 0.5) } /* ) */ /* return SmoothieArrayBuilder.buildBlock(v0, v1) */ }
-
39:22 - Smoothie initializer (final) and SmoothieBuilder (initial)
extension Smoothie { init(id: Smoothie.ID, title: String, @SmoothieBuilder _ makeIngredients: () -> (String, [MeasuredIngredient])) { let (description, ingredients) = makeIngredients() self.init(id: id, title: title, description: description, measuredIngredients: ingredients) } } @resultBuilder enum SmoothieBuilder { static func buildBlock(_ description: String, components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) { return (description, components) } }
-
40:38 - Accepting different types
// Accepting different types Smoothie(…) /* @SmoothieBuilder */ { /* let v0 = */ "Filling and refreshing, this smoothie will fill you with joy!" /* let v1 = */ Ingredient.orange.measured(with: .cups).scaled(by: 1.5) /* let v2 = */ Ingredient.blueberry.measured(with: .cups) /* let v3 = */ Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) /* return SmoothieBuilder.buildBlock(v0, v1, v2, v3) */ }
-
41:01 - Smoothie initializer (final) and SmoothieBuilder (initial)
extension Smoothie { init(id: Smoothie.ID, title: String, @SmoothieBuilder _ makeIngredients: () -> (String, [MeasuredIngredient])) { let (description, ingredients) = makeIngredients() self.init(id: id, title: title, description: description, measuredIngredients: ingredients) } } @resultBuilder enum SmoothieBuilder { static func buildBlock(_ description: String, components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) { return (description, components) } }
-
42:43 - SmoothieBuilder without the string
// SmoothieBuilder without the string Smoothie(…) /* @SmoothieBuilder */ { // "Filling and refreshing, this smoothie will fill you with joy!" /* let v0 = */ Ingredient.orange.measured(with: .cups).scaled(by: 1.5) /* let v1 = */ Ingredient.blueberry.measured(with: .cups) /* let v2 = */ Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) /* return SmoothieBuilder.buildBlock(v0, v1, v2) */ } extension SmoothieBuilder { static func buildBlock(_ description: String, _ ingredients: ManagedIngredients...) -> (String, [ManagedIngredients]) { … } }
-
43:38 - How Swift improves diagnostics
// How Swift improves diagnostics func fn0() throws {} func fn1() rethrows {} func fn2() {} func fn3() deinit {} func fn4() try {}
-
44:30 - SmoothieBuilder without the string
// SmoothieBuilder without the string Smoothie(…) /* @SmoothieBuilder */ { // "Filling and refreshing, this smoothie will fill you with joy!" /* let v0 = */ Ingredient.orange.measured(with: .cups).scaled(by: 1.5) /* let v1 = */ Ingredient.blueberry.measured(with: .cups) /* let v2 = */ Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) /* return SmoothieBuilder.buildBlock(v0, v1, v2) */ } extension SmoothieBuilder { static func buildBlock(_ description: String, _ ingredients: ManagedIngredients...) -> (String, [ManagedIngredients]) { … } @available(*, unavailable, message: "missing ‘description’ field") static func buildBlock(_ ingredients: ManagedIngredients...) -> (String, [ManagedIngredients]) { fatalError() } }
-
44:55 - Smoothie initializer (final) and SmoothieBuilder (with error handling)
extension Smoothie { init(id: Smoothie.ID, title: String, @SmoothieBuilder _ makeIngredients: () -> (String, [MeasuredIngredient])) { let (description, ingredients) = makeIngredients() self.init(id: id, title: title, description: description, measuredIngredients: ingredients) } } @resultBuilder enum SmoothieBuilder { static func buildBlock(_ description: String, components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) { return (description, components) } @available(*, unavailable, message: "first statement of SmoothieBuilder must be its description String") static func buildBlock(_ components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) { fatalError() } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。