ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swiftのジェネリクスを活用する
ジェネリクスは、Swiftで抽象コードを書き込むための基本的なツールです。コードの進化に応じて抽象化の機会を見きわめたり、多数の動作を1つのコードで記述する戦略を評価したり、Swift 5.7の言語特質を見つけて、ジェネリクスコードを記述しやすくしたり理解しやすくしたりする方法について解説します。
リソース
関連ビデオ
WWDC23
WWDC22
-
ダウンロード
♪ 落ち着いた雰囲気のヒップホップ音楽 ♪ ♪ Swift Compiler teamのHollyです 『Swiftのジェネリクスを活用する』へ ようこそ ジェネリクスは Swift で抽象コードを 書くための基礎的なツールで コードの進化に伴い発生する 複雑性の管理に欠かせません 抽象化は特定の詳細からアイデアを分離します コードでは多くの方法で抽象化が 役に立ちます 頻繁に使用する可能性がある抽象化の1つは コードを 関数やローカル 変数に抽出する場合です これは同じ機能や値を複数回 使用する必要がある場合に非常に便利です 機能を関数に抽出する場合 詳細は無視され 抽象化を使用するコードは 詳細を繰り返さずに何が起こっているかを示す アイデアを表現できます Swift では具象型を無視することもできます 同じアイデアで異なる詳細を持つ 一連の型に対し抽象コードを書き すべての具象型で実行することができます 本日は具象型を使って コードをモデル化し 一連の具象型で共通の一般的機能を特定し これらの機能を表現する インターフェースを構築し 最後に ジェネリックコードを書く方法をお見せします Swift の抽象化ツールを詳細に探り コードを構築して農場を模倣してみます では最初に 具象型を作成しましょう 「Cow」というstruct から始めます Cow には「eat」というメソッドがあり Hay という型のパラメータを受け入れます Hay も struct です 「grow」という静的メソッドがあり Hay を生産する作物を育てます これを Alfalfa と呼びます Alfalfa struct はAlfalfa のインスタンスから Hay を収穫するメソッドがあります 最後に「Farm」というstruct を追加します これには Cow にエサをやるメソッドがあります エサを与えるメソッドを実装するには 最初に alfalfa を育ててHay を生産し Hay を収穫してから Hay を Cow に食べさせます すると農場で Cow に エサを与えることができます しかし 他の動物も加えたいとします そこで Horse やChicken など他の動物を 表現する struct を追加します また農場で Cow Chicken Horse に エサをあげたいとします エサを与えるメソッドを多重定義して 各パラメータの型を別々に承認できますが その場合各多重定義に非常に よく似た実装が行われます そうすると他の動物を追加すると共に 余分なボイラープレートとなってしまいます また ほとんどのコードは反復的なものです 反復的な実装で 多重定義を書いている場合 一般化することが最適でしょう 基本的にこのような実装は 異なる動物が同様に機能するため 非常に似通っています 次のステップは異なる動物の間の 共通の機能を特定することです ここでは ある食料を食べることができる 異なる動物の型を作成しました 各動物は異なる方法で食料を食べます そのため各実装に対するeat メソッドは 動作が異なります ここで行いたいのは 抽象コードが接触メソッドを 呼び出すのを許可し 操作が行われている具象型によって その抽象コードが異なる 動作を行うようにします 抽象コードが異なる具象型に 異なる動作ができる能力を ポリモーフィズムと言い これは1つのコードがコードの使用方法によって 異なる動作を実行することを可能にします ポリモーフィズムにも異なる形式があります 1つは関数の多重定義です これは同じ関数の呼び出しが引数型によって 違う意味を持つことです 多重定義は 「アドホック多相」と呼ばれます なぜなら一般的な解決法ではないからです 先程 多重定義が反復的な コードにつながっていました 次はサブタイプ多相です コードがスーパータイプで操作され コードが実行時に使用する 特定のサブタイプによって 異なる動作を行うことができます 最後はパラメトリック多相で これはジェネリクスを使用して実現されます ジェネリックコードは型パラメータを使用して 異なる型で動作する一部の コードを書くことを許可し 具象型自体は引数として使用されます 多重定義はすでに除外したので サブタイプ多相を使用してみましょう サブタイプの関係を表現する方法に クラス階層があります 「Animal」というクラスを使います 次に各動物の型を struct から class に変更します 各動物クラスはAnimal スパークラスから 継承され eat メソッドをオーバーライドします ここですべての動物型を表現できる 抽象ベースのクラスAnimal があります Animal クラスでeat を呼び出すコードで サブタイプ多相を使用し サブクラスの実装を呼び出します しかしこれで完了ではありません 動物の eat メソッドのパラメータを まだ入力していません さらにこのコードには他にも赤信号があります まずクラスを使用したため 異なる動物インスタンス間で 状態を共有する必要がないのに 参照セマンティクスが強制されました またこの戦略にはベースクラスの オーバーライドメソッドに サブクラスが必要となり これを忘れた場合実行時まで わかりません この抽象化モデルのより大きな問題は 各動物のサブタイプは異なる種類の食物を食べ この依存性をクラス階層で 表現するのは非常に難しい点です ここで行える方法の1つはメソッドが Any など あまり具象でない型を承認させる方法です しかしこの戦略は 実行時に 正しい型がパスされるのを 確実にするためサブクラスの 実装に依存するので 各オーバーライドメソッドに 追加の ボイラープレートを強要しましたが 誤って違う種類の食物をパスすることになり 実行時にしかわからない別のバグが 発生してしまいます そのため他の方法を行いましょう 代わりに Animal のスーパークラスに 型パラメータを導入しエサの種類を 型安全な方法で表現します この種類の型パラメータは 各サブクラスの特定の エサの種類のプレースホルダとなります このアプローチではFood 型パラメータを Animal クラスの宣言に 引き上げる必要があります 少し不自然に感じますね 動物が動作するには食物は必要ですが 動物の中核目的は エサを 食べることではないからです また 動物に作用する多くのコードも 食物のことをまったく気にしていないでしょう にも関わらず Animalクラスの参照のすべてで 食物タイプを指定する必要があります 例えば各 Animal サブクラスは 継承句でカギ括弧の中に具象な 食物型を指定する必要があります 各動物に特定した型をさらに追加すると このAnimal クラスの各ユースサイトでの ボイラープレートは面倒なものになります これらのすべてのアプローチには 優れた人間工学や 適切な セマンティクスがありません 問題の根源はクラスがデータ型であることで スーパークラスをたたみ込み具象型についての 抽象的なアイデアを表現しようとしています その代わりに機能の仕組みの詳細なしで 型の機能を表現するように設計された 言語構築が必要です 動物には2つの共通の能力があります 各動物には特定の食物があり その食物を消費するための演算子があり インターフェースを構築して これらの機能を表現します Swift ではプロトコルを使い これを行います プロトコルは抽象化のツールで 適合する型の機能を説明します プロトコルを使用すれば実装の詳細から 型の作業に関するアイデアを 分離することができます アイデアは型が行う作業で インターフェースを通じて表現されます では 動物の機能をプロトコル インターフェースに変換しましょう プロトコルの名前は 説明する型のカテゴリを表します なのでこのプロトコルを「Animal」と呼びます 各機能はプロトコルの要件に マッピングされます 特定の型の食物はAnimal プロトコルに 関連型にマッピングされます 型パラメータのように関連の型は 具象型のプレースホルダになります 関連型が特別な理由は プロトコルに適合する 特定の型に依存する点です この関係は保証され 動物の特定の型の各インスタンスは 常に同じ型の食物を所有します 摂食するための演算子は メソッドにマッピングされ このメソッドは「eat」と呼ばれ 動物のエサの型のパラメータを承認します プロトコルにはこのメソッドの実装はなく 実装するには具象な動物の型が必要になります Animal プロトコルができたところで それぞれの具象な動物の型を適合させます 宣言または拡張時にプロトコル適合で 具象型を注釈することができます プロトコルはクラスに制限されないため structs enums actorsに プロトコルを使用できます 適合の注釈を書き終えたら コンパイラは具象型が それぞれのプロトコル要件を 実装したかを確認します 各動物型は eat メソッドを実装する必要があり コンパイラはパラメータリストでエサの型が 使用されているため推論することができます エサの型は型エイリアスを使って 明確に書くこともできます 動物の共通の機能を 特定し プロトコルインターフェースを使って その機能を表現できました ここでジェネリックコードを 書き始めることができます Animal プロトコルを使用して 農場で feed メソッドを実装できます すべての具象な動物の型に機能する 実装を1つだけ書きます ここではパラメトリック多相を使い メソッドが呼び出された時に 具象型の代わりとなる 型パラメータを導入します 型パラメータは機能名の後でカギ括弧の中に 書きます 通常の変数や関数パラメータと同様で 好きなように名前を付けることができます 他の型と同じように その名前を使って関数シグネチャ全体で 型パラメータを参照することができます ここでは「A」という型パラメータを宣言し 動物の関数パラメータの型として使用します Animal プロトコルへの適合では 常に具象な動物の型を使用します なので型パラメータをプロトコル適合性で 注釈します プロトコル適合性はカギ括弧の中か 「Where」句の後に書くことができ 異なる型パラメータの間で 関係を指定することができます 名前のついた型パラメータと 「Where」句の追跡は 非常に強力ですなぜなら洗練された要件や 型の関係を書けるようになるからです しかしジェネリック機能の ほとんどには必要ありません feed メソッドに焦点を当てましょう 型パラメータ A はリストに一度表示され 「where」句は型パラメータの適合条件を 表示します この場合 型パラメータに名前を付け 「where」句を使用したため メソッドが必要以上に複雑に見えます このジェネリックパターンは一般的なもので よりシンプルな方法で表現することができます 型パラメータを明確に表現する代わりに プロトコル適合性で 「some Animal」と書くことで 抽象的な型を表現します この宣言は以前と同一のものですが 不必要な型パラメータリストや 「where」句がなくなっています それらが提供する表現力が 必要ではないからです 「some Animal」と書く方がより直接的です なぜなら 構文ノイズが低減し パラメータ宣言に直接 動物パラメータに関する セマンティクス情報が含まれるからです Animal 構文を詳しく見てみましょう 「some Animal」の「some」は 取り組んでいる特定の型があることを 示しています 「some」キーワードは常に適合条件が 後に続きます この場合Animal プロトコルに 適合する必要がありパラメータ値で Animal プロトコルからの要件を 使うことができるようになります 「some」はパラメータと結果型で使用できます SwiftUI コードを書いたことがあるなら 結果の位置で「some view」を使用し 「some」をすでに使用しているでしょう 結果型の「some View」も完全に同じ概念です SwiftUI ビューではbody プロパティは 特定の型のビューを返しますが その body プロパティを使用するコードは どの特定の型かを知る必要はありません では特定の抽象型の概念を より良く理解するために少し戻ってみましょう 特定の具象型向けにプレースホルダを表す 抽象型は不透明型と呼ばれています 代用される特定の具象型は 基本型と呼ばれます 不透明型の値では 基本型は値の範囲向けに固定されます これにより値を使用するジェネリックコードは 値がアクセスされるたびに同じ基本型を得ます 「some」を使用する型と カギ括弧内の名前が付いた 型パラメータはどちらも不透明型を宣言します 不透明型は出入力の両方で使用でき どちらもパラメータの位置または結果の位置で 宣言することができます 関数アローはこれらの位置を 分割します 不透明型の位置で プログラムのどちらの部分が抽象型を確認し どちらが具象型を決定するかが決まります 名前付き型パラメータは常に入力を宣言し 呼び出し元が基本型を決定し 実装は抽象型を使用します 一般的に プログラムの不透明パラメータまたは 結果型に値を供給する部分が 基本型を決定し値を使用するプログラムの 部分が抽象型を確認します この仕組みをより深く見てみましょう パラメータと結果値に対する直感を使います 基本型は値から推論されているため 基本型は常に値と同じ場所から やってきます ローカル変数においては基本型は割当の右側の 値から推論されます これは不透明型のローカル変数には 初期値が常にある必要があることを意味します それを提供しない場合 コンパイラはエラーを報告します 基本型は変数範囲向けに 固定される必要があり 基本型の変更を試みようとしても エラーにつながります 不透明型のパラメータでは基本型は 呼び出しサイトでの引数値から推論されます パラメータ位置での「some」の使用は今回初で 基本型の固定が必要になるのは パラメータの範囲だけとなり 各呼び出しは異なる引数型を 提供することができます 不透明の結果型では基本型は実装の 戻り値から推論されます 不透明な結果型のメソッドや 計算済みのプロパティは プログラムのどこからでも呼び出すことができ この名前が付いた値の範囲は世界共通になり 基礎の戻り型はすべての return 文で 同じである必要があります 同じでない場合コンパイラはエラーを報告し 基礎戻り値に一致しない型があることを示します 不透明 SwiftUI ビューではViewBuilder DSL は 制御フローのステートメントを各枝が 同じ基礎戻り型を持つように 変換することができます この場合ViewBuilder DSL を 使用して 問題を解決することができます メソッドで @ViewBuilder注釈を書き 戻りステートメントを削除すると ViewBuilder 型で結果が 構築されるようになります feedAnimal メソッドに戻りましょう パラメータリストで「some」を使用できます なぜなら他の場所で不透明型を 参照する必要がないからです 不透明型を関数シグネチャで複数回 参照する必要がある場合 名前型パラメータが役立ちます 例えば 動物のプロトコル「Habitat」に 別の関連型を追加した場合 農場で所定の動物向けに Habitatを構築することが理想的です この場合 結果型は特定の動物型に依存しているため 型パラメータ A をパラメータ型と 戻り型で使用する必要があります 不透明型を複数回参照する場合がある 別の場所はジェネリック型です コードはジェネリック型で 型パラメータを宣言し 適切に保管するために型パラメータを使用し memberwise initializer でも使用することがあります ジェネリック型を異なる文脈で参照するには カギ括弧内で型パラメータを明確に指定する 必要があります 宣言のカギ括弧は ジェネリック型の使用方法を 明確にするのに役立ち 不透明型をジェネリック型に 由来して名付けます では feed メソッドの実装を 構築しましょう 動物パラメータの型を使用し feed 関連型を通じて 成長させたい収穫型にアクセスします これを Feed.grow() と呼び このエサの型を 生産するインスタンスを取得します 次に 収穫から農産物を生産する必要があります これは「harvest」と呼ばれる収穫型が 提供するメソッドを通じて行います 最後にこの農産物を 動物に与えることができます 基礎的な動物型が固定されているため コンパイラはさまざまな メソッド呼び出しを通じて植物型 農産物型 動物型間の関係を把握しています これらの静的な関係により動物に 間違ったエサを与えることを 防ぐことができます 特定の動物に対し正確な食物型であることが 保証されていない型を使用しようとすると コンパイラはそれを教えてくれます 農場の他のプロトコルを学ぶために 動物のエサ型と植物間の 関係を表現する方法についてのトークは 『Swiftでプロトコル インターフェイスを設計する』で 最後にすべての動物にエサを あげるメソッドを追加します 配列を承認する feedAll と いうメソッドを追加します 要素型はAnimal プロトコルに 適合する必要がありますが 配列には異なる型の動物を 保管することを希望しています いずれかの動物が協力して くれるか 見てみましょう 「some」には変化させることができない 特定の基本型があります 基本型が固定されているため 配列のすべての要素には 同じ型が必要になります 一部の Animal の配列は適切な表現をしません 私が異なる動物型を含める 配列を求めているからです ここではあらゆる動物を表現できる スーパータイプが必要です そこで「any Animal」と書くことで 動物を 任意型で表現します 「any」キーワードはこの型が動物のすべての 任意型を保管できることを示します また動物の基本型が実行時に 変化することができます 「some」キーワードのように 「any」も適合条件が 後に続きます any Animal は単一の静的型で どんな動物の 具象型でも動的に保管できるため 変数型でサブタイプ多相を 使用できるようになります この柔軟性のある保管を実現するために any Animal 型にはメモリに特別な表現が あります この表現を箱だと考えてください 時には 値が小さく 直接箱に収まることができます 値がこの箱には大きすぎることもあり その場合値は他のどこかで分割され 箱はその値のポインタとなります 動物の具象型を動的に保管できる 静的型は正式には 存在型と呼ばれます 同じ表現を異なる具象型で使用する 戦略は「型消去」と呼ばれています 具象型はコンパイル時間に消去され 具象型は実行時にのみ知ることができます 存在型のこの2つのインスタンスでは動物の 静的型は同じですが動的型は異なります 型消去は異なる動物の間の型レベルの特徴を 除外するため異なる動的型の値を 同じ静的型として 交互に使用できることができます 型消去を使用して値型の異種配列を 書くことができます feedAll メソッドでこれを行います any Animal の配列をパラメータ型として 使用します 型に関連するプロトコルに 「any」を使用するのは Swift 5.7. では新しいことです feedAll メソッドを実装するには まず動物の配列を反復します 各動物に対しeat メソッドを Animal プロトコルから呼び出します このメソッドを呼び出すには この反復の基礎動物の特定の feed 型を 取得する必要があります しかし any Animal でcall を呼び出した瞬間に コンパイラのエラーが発生します 特定の動物型の間の型レベルの特徴を 除外したために 関連型を含む 特定の動物型に依存するすべての関係が 除外されてしまい この動物がどのエサを期待しているか不明です 型関係に依存するには 特定の動物型が固定されている 文脈に戻る必要があります any Animalで 直接eat を呼び出す代わりに some Animal を使用する必要があります any と some では型が異なりますが コンパイラは基礎の値をany Animalの 箱から取り出してsome Animal パラメータに パスすることで some Animal に変換できます 引数を箱から出す機能も Swift 5.7.の新しい機能です これはコンパイラが箱を空けて 中に保管されている 値を取り出すようなものだと考えてください Animal パラメータの一部では 基本型の値は固定されているので 基本型のすべての演算子に アクセスすることができ それには関連型へのアクセスも含まれます これはすばらしい機能です なぜなら必要に応じて 柔軟に保管することができ 関数の範囲の基本型を固定することで 静的型システムの完全な表現性がある 文脈に戻ることも できるからです またほとんどの場合 箱から取り出すことを考える必要はありません 期待通りに動作するからです any Animalでプロトコル メソッドを呼び出すように 基本型でメソッドを呼び出します なので各動物を feedメソッドにパスし 適切な農産物を育てて収穫し それぞれの反復で特定の 動物にエサを与えられます このプロセスを通して「some」と「any」には 異なる機能があることを知りました 「some」では基本型は固定されており 型関係でジェネリックコードの 基本型に依存できるようになり 作業をしているプロトコルで API と関連型に完全アクセスできます 「any」は任意の具象型を 保管する場合に使用します 「any」は型消去を提供し 異種コレクションの表現や オプションを使用して 基本型の不在を表現し 抽象化と実装の詳細を表現します 一般的にはデフォルトで「some」を書き 任意値を保管する必要が ある場合に「some」を「any」に変更します このアプローチなら費用を払う必要があるのは 型の消去とセマンティクスの制限で 保管の柔軟性が必要となる場合のみです このワークフローは変化が必要だと気づくまで デフォルトの let 定数の書き込みに似ています このセッションでは コードが進化し機能が増加した時の コードの一般化を説明しました 最初に具象型を書き コードが機能性を増すと共に 異なる具象型の間の反復に気づきました そこから共通の機能を特定し プロトコルを使用して一般化しました 最後に「some」と「any」を 使用して抽象コードを書き より表現的なコードには 「some」を推奨しました プロトコル作成の詳細についてや 型消去の説明については 『Swiftでプロトコルインターフェイスを 設計する』をご覧ください ありがとうございました WWDC をお楽しみください ♪
-
-
27:10 - Complete example
protocol AnimalFeed { associatedtype CropType: Crop where CropType.Feed == Self static func grow() -> CropType } protocol Crop { associatedtype Feed: AnimalFeed where Feed.CropType == Self func harvest() -> Feed } protocol Animal { associatedtype Feed: AnimalFeed func eat(_ food: Feed) } struct Farm { func feed(_ animal: some Animal) { let crop = type(of: animal).Feed.grow() let produce = crop.harvest() animal.eat(produce) } func feedAll(_ animals: [any Animal]) { for animal in animals { feed(animal) } } } struct Cow: Animal { func eat(_ food: Hay) {} } struct Hay: AnimalFeed { static func grow() -> Alfalfa { Alfalfa() } } struct Alfalfa: Crop { func harvest() -> Hay { Hay() } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。