20
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SwiftAdvent Calendar 2019

Day 2

[Swift] AnyPokemonリターンズ ~型消去型の弁明~

Last updated at Posted at 2019-12-01

Swift Advent Calendar 2019 2日目の記事です。

記事を書いた時の環境

2019: Swift 5.1

書いたこと

  • 型消去型とは
  • 型消去型の大変心苦しい立場

型消去型とは

AnyPokemon について

AnyPokemonは、平常心で型を消しさるで紹介された型消去型です。

これを変更して、型消去型とは何なのか、どうして必要なのかを考えてみます。

AnyPokemon おさらい

ポケモンは、雷属性ポケモン、炎属性ポケモンのように属性によって分類することが出来ます。

この属性の違いを型として表す場合、オブジェクト指向で考えれば、ポケモンクラスからサブクラスとして派生させるアプローチが一般的です。

ですが、Swiftのstructは継承が許されていません。
そこでProtocolを用います。(※1)

Pokemonプロトコル
// 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の登場です。

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つです。

  1. 型宣言することが出来る
  2. Pokemonプロトコルである

つまり型消去型とは何らかのプロトコルの具体型であるといえます。

型消去型 in 標準ライブラリー

標準ライブラリーに用意されている型消去型は、Any~ と命名されています。
その中で以下の2つの型消去型をご紹介します。

  • AnyHashable
  • AnySequence

AnyHashable

AnyHashableは、その名の通り、Hashableプロトコルの具体型です。

SwiftのDictionaryは、同時に複数の型をキーとして取るこことが出来ません。

// コンパイルエラー
let desc = ["😵" : "emoji", 42 : "an Int"];

キーに複数の型のインスタンスを受け付けるようにするには、型宣言としてHashableプロトコルを宣言する必要がありますが、Selfassociatedtypeを持ちますので型宣言に用いることは出来ません。

Hashableを型宣言に用いることが出来ない
// NG
let desc:[Hashable: String] = ["😵" : "emoji", 42 : "an Int"];

そこでAnyHashableの出番です。

// OK
let desc = ["🥳" : "emoji", 42 : "an Int"] as [AnyHashable : Any]

つまりAnyHashableを使えば、個々の型を消去し、AnyHashable型としてまとめることが出来ます。

AnySequence

AnySequenceSequenceプロトコルの具体型です。
こちらも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>) {
    ....
}

またジェネリクスは自分で定義する必要もありますが、標準ライブラリーには必要となる型消去型が沢山用意されています。

まとめ

  • Selfassociatedtypeを持つProtocolの具体型を実現する
  • ジェネリクスが使えないローカルの変数の宣言に使うことが出来る
  • 型消去型を使う利点
    • 接続がしやすくなる
    • ジェネリクスよりも可読性が高い
    • ジェネリクスと違って沢山の型消去型が用意されている

Appendix

型消去 (Type erasure)型消去型 (type-erased type) について

型消去という名詞は、Javaでも使われていますが、ここでの型消去とは意味が異なります。
(Javaの型消去は、JVM上では型が消去されることを意味します。)
ここでの型消去は、type-erased という形容詞になります。後ろにtypeという名詞が続いています。

まぁ型消去は名詞なのでどちらに使っても間違いではないと思いますが、より正確に言うならば型消去型(type-erased type)です。

structとenumについて

※1から、Swiftのstructは、直積タイプの代数的データ型であることを意味します。またenum直和タイプの代数的データ型です。

参照

型消去の話で出てきたポケモンの例題を理解する #tryswiftconf

型消去 in Swift

20
10
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?