Swift Advent Calendar 2019 2日目の記事です。
記事を書いた時の環境
2019: Swift 5.1
書いたこと
- 型消去型とは
- 型消去型の大変心苦しい立場
型消去型とは
AnyPokemon について
AnyPokemonは、平常心で型を消しさるで紹介された型消去型です。
これを変更して、型消去型とは何なのか、どうして必要なのかを考えてみます。
AnyPokemon おさらい
ポケモンは、雷属性ポケモン、炎属性ポケモンのように属性によって分類することが出来ます。
この属性の違いを型として表す場合、オブジェクト指向で考えれば、ポケモンクラスからサブクラスとして派生させるアプローチが一般的です。
ですが、Swiftのstruct
は継承が許されていません。
そこでProtocolを用います。(※1)
// Pokemonプロトコルは属性を表すtypeを持つ
protocol Pokemon {
// 属性を表す型
associatedtype PokemonType
var type:PokemonType { get }
}
Pokemonプロトコルを用いると、雷属性のピカチューは以下のように表すことが出来ます。
// 雷属性を表す
struct Electric {}
// ピカチュー
struct Pikachu: Pokemon {
var type = Electric()
}
しかしPokemonプロトコルは型宣言として用いることが出来ません。
// NG 型宣言としては使えない
var pokemon:Pokemon = Pikachu()
これはSwiftの言語仕様として、Self
もしくは associatedtype
を持つprotocol
は型宣言として使用することが出来ないためです。
そこでAnyPokemonの登場です。
struct AnyPokemon<PokemonType>:Pokemon {
var type: PokemonType
{
return self._type
}
var rawType: PokemonType
init<T:Pokemon>(_ pokemon: T) where T.PokemonType == PokemonType
{
self.rawType = pokemon.type
}
}
AnyPokemonを使えば、雷属性のポケモンを受け取る型を宣言することが出来ます。
// OK
let electricPokemon:AnyPokemon<Electric> = AnyPokemon(Pikachu())
// Arrayの型変数としてもOK
let electrics:[AnyPokemon<Electric>] = [AnyPokemon(Pikachu()), AnyPokemon(Raichu())]
AnyPokemonの注目ポイントは2つです。
- 型宣言することが出来る
- Pokemonプロトコルである
つまり型消去型とは何らかのプロトコルの具体型であるといえます。
型消去型 in 標準ライブラリー
標準ライブラリーに用意されている型消去型は、Any~ と命名されています。
その中で以下の2つの型消去型をご紹介します。
- AnyHashable
- AnySequence
AnyHashable
AnyHashable
は、その名の通り、Hashable
プロトコルの具体型です。
SwiftのDictionaryは、同時に複数の型をキーとして取るこことが出来ません。
// コンパイルエラー
let desc = ["😵" : "emoji", 42 : "an Int"];
キーに複数の型のインスタンスを受け付けるようにするには、型宣言としてHashable
プロトコルを宣言する必要がありますが、Self
、associatedtype
を持ちますので型宣言に用いることは出来ません。
// NG
let desc:[Hashable: String] = ["😵" : "emoji", 42 : "an Int"];
そこでAnyHashableの出番です。
// OK
let desc = ["🥳" : "emoji", 42 : "an Int"] as [AnyHashable : Any]
つまりAnyHashable
を使えば、個々の型を消去し、AnyHashable
型としてまとめることが出来ます。
AnySequence
AnySequence
はSequence
プロトコルの具体型です。
こちらもSelf
,assciatedtype
を持つため型宣言として利用することが出来ないため、AnySequence
を使うことが出来ます。
// OK
var numbers: AnySequence<Int> = AnySequence([1, 2, 3, 4, 5])
ですが、上記AnySequenceを用いる理由は全くありません。
ただ[Int]
と宣言すればいいだけです。。。
それではAnySequenceはどういった場面において、有効的な使い方が出来るのでしょうか?
それを考えてみます。
型消去型の大変心苦しい立場
型消去型には妬ましい強敵が存在します。
ジェネリクスです。
そのため影に追いやられている大変辛い存在になってしまっています。
なんとか日陰から日の当たる場所に出してあげたいと思います。
そこで一旦ジェネリクスの存在を無視し、AnySequence
を使って型消去型の意義を考えます。
型消去型の意義を考える
文字列をトークンに変換する関数を考えます。
func convertToken(from str: [Character]) -> Token { ...
この関数を文字列から直接呼び出す場合、Array型を生成するコスト**O(N)**が発生します。
// 呼び出すたびにコストO(N)が発生する
let token = convertToken(Array("◎だこのたこ焼きを食うと胸がやける🤬"))
それならば、文字列として宣言すればいいじゃない?
func convertToken(from str: String) -> Token { ...
では文字列をソートしてからトークンに変換するとなったとき、どうしましょうか?
// sortedメソッドの返り値は[Character]
let token = convertToken(from: String(str.sorted()))
一旦、配列に変換された文字列を再度文字列に変換する必要が発生します。
そこでAnySequence
の出番です。
func convertToken(from chars: AnySequence<Character>) -> Token { ...
// 文字列から直接
let token = convertToken(from: AnySequence(str))
// ソートしてから
let token2 = convertToken(from: AnySequence(str.sorted()))
このように受け取れる型の定義域を広げることで、接続をしやすくすることが出来ます。
これは型が非常に強いプログラミング言語においてはとても重要なことです。
ジェネリクスとの比較
残念ながら型消去型を積極的に使われることは殆どありません。
なぜならばジェネリクスを使えば、コンパイル時点で、呼び出し箇所で型を決定することができ、最適化の恩恵も受けることが出来るからです。
強いて言うならば、ジェネリクスは読み解くのが難しいと言えるかもしれません。
以下電撃属性のポケモンを受け取る関数を見比べてみれば、一目瞭然です。
// ジェネリクス
func pokemonWithElectric<U>(_ pokemon: U) where U:Pokemon, U.PokemonType == Electric {
....
}
// 型消去型
func pokemonWithElectric(_ pokemon: AnyPokemon<Electric>) {
....
}
またジェネリクスは自分で定義する必要もありますが、標準ライブラリーには必要となる型消去型が沢山用意されています。
まとめ
-
Self
、associatedtype
を持つProtocolの具体型を実現する - ジェネリクスが使えないローカルの変数の宣言に使うことが出来る
- 型消去型を使う利点
- 接続がしやすくなる
- ジェネリクスよりも可読性が高い
- ジェネリクスと違って沢山の型消去型が用意されている
Appendix
型消去 (Type erasure) と 型消去型 (type-erased type) について
型消去という名詞は、Javaでも使われていますが、ここでの型消去とは意味が異なります。
(Javaの型消去は、JVM上では型が消去されることを意味します。)
ここでの型消去は、type-erased という形容詞になります。後ろにtypeという名詞が続いています。
まぁ型消去は名詞なのでどちらに使っても間違いではないと思いますが、より正確に言うならば型消去型(type-erased type)です。
structとenumについて
※1から、Swiftのstruct
は、直積タイプの代数的データ型であることを意味します。またenum
は直和タイプの代数的データ型です。