明確な型を持つことがSwiftらしい方法であると気づいたとき、時には型を消去することが必要であることがわかります。 この講演(try! Swift)では、Gwendolyn Westonが型とは何か、また型を消去することが何を意味するのか、また、なぜそうしたくなるか説明します。
平常心で型を消し去る (00:00)
型消去について話していきます。
最初は怖いと思うかもしれません。 Swiftはタイプセーフ(型安全)な言語です。なぜ型を消去したいと思うのでしょうか。あるいは、型を消去することにどんな意味があるのか混乱するかもしれません。 または、最終的に型とは何かという基本的なことを自分自身に問いかけるかもしれません。
型とは何ですか? (00:39)
型とは何ですか?(長い休止)それは日常のコーディングにとって必須ですが、定義を思い出せませんでした。
最高の定義はウィキペディアにありました。型とは一組の値を定義する分類であり、それらの値に対する演算です。 例えば、Int
型は整数(負の無限大から正の無限大まで)です。また、それに対する演算は加算、乗算および剰余演算子(%
)です。
コンパイラといくつかの型について (01:53)
この概念はSwiftコンパイラにとって有効です:この制約により、Int
型の何かをString型にセットしたり、ディクショナリのindexOf
メソッドをコールすることに意味があるのかどうかわかります。
すべての型が必ずしも同じように作られるとは限りません。Swiftでは、コンパイラが構文をチェックできない不完全な型を持つことができます。
2種類の型:具象型と抽象型 (02:28)
具象型は明確な実装を持っています。それらのメソッドはすでに要件を満たしており、直接インスタンスを生成することができます。 さらに具象型はデータとして表現されます。それらは情報を共有するために、オブジェクト間で渡されます。
例えば、次の2つは具象型です:
let concreteInt = 42
let concreteArray = ["much", "concrete", "wow"]
次に、抽象型で不完全な実装の型があります。それらは定義中にプレースホールダー型を持っていて、直接インスタンス化できません。
抽象型は振る舞いを表現しています。挙動が格納される値より重要なところでオブジェクトを表わすためにそれらを使用します。例えば、私たちは抽象型を定義しました:
class GenericClass<T> { ... }
let object: GenericClass<T>
struct GenericStruct<U> { ... }
let object: GenericStruct<U>
それは、あるT
型のジェネリックなクラスとあるU
型のジェネリックな構造体です。 しかし、それらをインスタンス化、または宣言することはできません。SwiftコンパイラはT
が何であるかわからないので、T
型のジェネリッククラスのインスタンスを作ることができないのです。 構造体でもどんなU
がどれであるかわからないので、U
型のジェネリックな構造体のインスタンスを作るすることができません。 これをどのように解決しますか?抽象型を使用するために、どのように具象化しますか?
抽象型を具象化するには? (04:19)
型の具象化 (04:27)
型の具象化の出番です:プレースホールダーの型を記述することにより抽象型の具象化を行います。最初は混乱するかもしれませんが、それが何であるかは既によく知っています。皆さんは恐らく頻繁に型パラメータ使用するでしょう。 例えば:
no good + <T>
= OK!
class GenericClass<T> { ... }
let StringClass: GenericClass<String>
struct GenericStruct<T> { ... }
let IntStruct: GenericStruct<Int>
ジェネリッククラスT
を定義しました。それをインスタンス化するとき、String
型のパラメータを渡すことによって、String
型のジェネリッククラスとしてインスタンス化します。構造体の場合でも同じことです。未定義の型T
のジェネリックな構造体として宣言しているにもかかわらず、インスタンス化するときはInt
型のパラメータを渡しています。これらはコンパイルされ、抽象的な型をインスタンス化することができます。
型パラメーター (04:46)
型パラメーターを使用することができない唯一の例外: protocol + <T
> = no good
それらがジェネリックである場合、型パラメータを指定するために、プロトコルを使用することはできません。代わりに、ジェネリックなプロトコルは次のようになります:
protocol Pokemon {
typealias PokemonType
func attack(move:PokemonType)
}
class Pikachu: Pokemon {
func attack(move: Electric) { ⚡️ }
}
class Charmander: Pokemon {
func attack(move: Fire) { 🔥 }
}
Pokemon
と呼ばれるジェネリックなプロトコルがあります(ポケモンはお互いに戦うことができるようなエレメント持っている小さなモンスターです)。PokemonType
というtypealias
キーワードで示されるジェネリック型があります。PokemonType
というジェネリック型の引数を取るattack
というメソッドがプロトコルにあります。その下には、このプロトコルを実装する2のクラスがあります。Pikachu
、攻撃動作はElectric
型、Charmander
、攻撃動作はFire
型です。理想的にはPokemon
型の何かを宣言してポケモン間の戦闘オブジェクトを作成し、selectedAttack
に基づいて攻撃したいと思います:
let pokemon: Pokemon
pokemon.attack(selectedAttack)
このコードのロジックでは、どのPokemon型を選択したかや、どのようなattack型であるかは重要ではありません。しかし、このコードはコンパイルできません。 Pokemonプロトコルは、selfまたはassociated型である必要があるため、ジェネリックの制約としてのみ使用できます。
というエラーが発生します。Pokemon
は抽象型なので、私たちはそれを宣言したり、直接インスタンス化できません。 Swiftコンパイラーはその理由を理解することができないでしょう。実装が事前にわからないことをSwiftコンパイラに伝える方法が必要ですが、この実装ではジェネリックなプロトコルの具体的なジェネリック型を作ることを保証することができます。
let pokemon: Pokemon
AnyPokemon (07:58)
AnyPokemon
というラッパークラスを作成しましょう:
class AnyPokemon <PokemonType>: Pokemon {
required init<U:Pokemon where U.PokemonType == PokemonType>(_ pokemon: U) {
}
}
このinitメソッドを使って初期化するときには、Pokemon
プロトコルを実装しているPokemonType
は、AnyPokemon
ラッパークラスが初期化されているPokemonType
パラメータと一致する場合のみ受け入れます。
このラッパー・クラスを使用して、次のようなコードを書くことができます:
let p1 = AnyPokemon(Pikachu())
let p2: AnyPokemon<Fire>
p2 = AnyPokemon(Charmander())
let digimon = AnyPokemon(NotAPokemon())
let pokemon = Pikachu()
vs.
let pokemon: AnyPokemon <Electric> pokemon = AnyPokemon(Pikachu())
直接Pikachu
インスタンスを渡してAnyPokemon
型をインスタンス化します。そして、それはPikachu
インスタンスから推論されるため、p1にはAnyPokemon<Electric>
型が格納されています。または、最初にAnyPokemon
型消去 (09:45)
しかしながら、もし(最初に)Pikachu
イニシャライザを用いてPikachu
インスタンスをインスタンス化した場合はPikachu
型になります。しかし、このAnyPokemon
ラッパークラスを使ってインスタンス化されているため、PikachuはAnyPokemon<Electric>
としてインスタンス化されます。つまり、型情報を消去しました(これが型消去を意味します)。
let pokemon: AnyPokemon <AnyObject>
pokemon = AnyPokemon(Pikachu()) // no good 🙅
プロトコルを実装したり、中身を実装しなくても、ジェネリック型を満たすオブジェクトをインスタンス化することを保証するために、ラッパークラスには慣例的に「Any」という接頭辞をつけます。
この型消去という名前そのものが混乱の元になると思います。型の情報を失うことがソリューションではなくて、ソリューションの兆しなんです(それがなぜうまくいくのかという理由ではありません)。作成した実装は必須情報を記述することが保証される構成なので、タイプスキャッフホールディングスような名前が良いと思います。また、型消去ではこのようなコードを書くことはできません。このオブジェクトをインスタンス化するために、AnyPokemon<AnyObject>
の何かを宣言し、インスタンス化するためにPikachu
インスタンスを渡すことができません。
共変性はありません (10:57)
Swiftは共変性をサポートしていないので、基本的には<Electric>
型は機能的に<AnyObject>
を代用することができるはずですが、ジェネリック型ではこれらを置き換えることができません。
マイナス面 (11:15)
ジェネリックなプロトコルを具象化するために、型消去を使用することには多少不利な点ががあります:
-
ボイラープレート: 実際、
AnyPokemon
のボイラープレート・コードのすべてをお見せしませんでした。(多くの記事がありますが、Hectorのブログに素晴らしい記事があり、大変おすすめです。) -
失われた型情報: 型消去は、ラッパーで何かをインスタンス化するときには、それが何であるか特定の実装がわからないことを意味します。それが
Electric
型またはFire
型であるということだけわかります。 - 共変性はありません:サブタイプを親タイプに置き換えることはできません。それはコードの柔軟性の妨げになります。
多分これは変更されるでしょう。既に、Swift2.2は、’typealias’キーワードは、’associatedtype’キーワードで置き換えることもできると発表しました。アップルの多くの人々はすでに、共変性をサポートし、Any
ラッパーのボイラープレートコードを自動的に生成することを表明しています。
まとめ (12:42)
私たちは次のことを学習しました:
- 型とは何ですか?
- 異なる2つの型(具象的、抽象的)
- 型パラメーターを使用して型を具象化する方法
- 型消去
Q&A (13:30)
Q: Q:以前にプロトコルと型が連携(protocols-with-associated-types)されている例を見てきましたが、扱いにくいものでした。型消去に関するその他の例を見たことがありますか?毎日のSwiftコードを楽しんでますか?
Gwendolyn: プロトコルにジェネリック型があるときに多く見受けられます。しかし、呼び出そうとしている関数にジェネリック型は必要ありません。 たとえば、attack
関数がPokemon
型を持っておらず、事前に型情報が必要なくても、それをコールした場合はコンパイルエラーになります。
また、制約を回避するだけのために型消去を使用する人もいます。しかし、これまでのところ、私が見たのはそれらの2つの実例だけです。
Q: 型というわけではありませんが、。エクステンションでこれを行うための何かよい解決策を見たことがありますか?。私はエクステンションがジェネリクス化できないのを知っていますが、ジェスチャー認識を例にしてみますと、多数のサブクラスを持つことができますが、それに接続する機能を追加することができます。あなたのリサーチでは何か解決策を見たことがありますか? Gwendolyn: 見たことはないですが、それを調査するのは良いと思います。
Q: Q: アップルは実際に標準ライブラリで型消去を使っていますか?
Gwendolyn: AnySequence
かAnyCollection
が型消去のラッパーだと思います。
Q: 素晴らしい講演でした。私達が理解できるように少し簡単で抽象的だった例の他に興味があります:あなたにこの調査をする気にさせたプロジェクト、あるいはビジネス上の問題、あるいはプロジェクトは遂行できたけども、解決しようとしていた問題がありますか。
Gwendolyn: はい、私が見た本番環境で型消去を行っていたのは私の元同僚の一人のBenji Enczです。彼はReSwiftと呼ばれるこのオープンソースライブラリーを持っています。
FacebookのFluxパターンやReactパターン、Facebookのいずれかのパターンは単一方向データフローに準じます。そして情報を出力するストアを持ち、その情報を取得(待機)するサブスクライバーがあります。彼が型消去を使った方法は、StoreSubscriber
というプロトコルを通じてサブスクライバーを定義したことでした。また、そのジェネリック型は何でもStoreSubscriber
になることができ、ストアから送信された情報を取得できました。
Q: 素晴らしい講演でした。私はもし貴方が状況を知っているなら、Swiftのより新しいバージョンで実装される共変性と反変性の話に言及するのではないかと思っていました。現状をご存知ですか?3.0での実現になるでしょうか? Gwendolyn: いえ、私はスケジュールについて知りません。しかし、私はその情報を得ています:Joe Groffは私にツイートしました。「私たちはまだ共変性をサポートしません」(これは昨年のことでした)。さらに、もう一人の元同僚はad-bonでたくさんの交流があり、彼は「共変性がなければ、型消去の用途が限定されてしまうので、彼らは共変性のサポートを切望しています。」と言っていました。”
Q: 私は型が好きではありません。型を学習する最良の方法は何でしょうか? Gwendolyn: あなたの質問は「型を使用する方法を学習する最良の方法は何ですか」でしたか?Swiftでいろんなことを1つ1つ書いてみてください。コンパイラーがエラーを返すことがありますが、あなたはそれが何故かわからないでしょう。それは一見正しく見えますが、「いいえ、それはオプショナルです。それはがnilまたはオプショナルでないことを確認する必要があります。それがnilではないことを確認する必要があります。」となります。Swiftアプリを書いて苦労していると、痛みを伴う厳しい試練のようですが、最後には「私はタイプセーフなコードを書くことができます、そして、非常に単純なSwiftアプリを書くのに3か月しかかかりませんでした。」となるでしょうw。”
Q: よい講演をありがとう。 私はセーフティなプログラマです。 私は型が好きです。 一言か二言で型消去についてのまとめをお願いできますか? Gwendolyn: ご質問は「型消去の定義をもっと簡潔にまとめてください」ですね。これは私がこの講演の準備中に苦労していたことです。 私が異なる4つの定義についてそれぞれ誰かに話すと、「いや、それは全く正しくない。いやいや、それはちょっと違うよ」と指摘され続けました。 私はベストな定義は、型消去は具象化しようとしているプロトコルの実装のインスタンスのみで初期化できるように、初期化メソッドに制約を持つラッパークラスがある、というデザインパターンであると感じています。 でも、もっとよい定義がほかにあれば教えてください。 私はそれを聞きたいです。 このボイラープレートコードはもう少し複雑です。 これはその縮小バージョンです。 このコードを理解しようとして2週間かかりました。 私は理解するまでに、多分2週間はかかったと思います。「なぜこんなことをしたいのか?」言い訳っぽい気もしますが、私はみなさんが「やったー、強くなったー」となるまで努力するべきだと思います。”
About the content
2017年3月のtry! Swift Tokyoの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。