型消去の使い方 in Swift

Swiftは非常に型が強くまたSelfassociatedtype を持つプロトコルは型宣言として使用することが出来ない。

例えばDictionary型は、異なる型のキーを同時に取ることが出来ない。

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

AnyHashable型

本来DictionaryのキーはHashableさえ満たしていれば良いので、型が同一である必要はない。 そこで 型消去ラッパー (type-erased Wrapper) と呼ばれる AnyHashable型 が用意されている。

// OK
let desc2 = [AnyHashable("🥴") : "emoji", AnyHashable(42) : "an Int"];

// OK AnyHashableは文字列リテラル、数字リテラルから変換可能
let desc3: [AnyHashable: String] = ["🥴" : "emoji", 42 : "an Int"];

型消去型ラッパーとボックス化するための型との違いは、 ある明確な主張を持っている ところにある。

例えば上記のAnyHashable を生成するには、Hashable プロトコルに準拠した値を渡す必要がある。

struct A {}
// NG 型AがHashableを満たしていないため
let a = AnyHashable(A())
struct A: Hashable {}
// OK
let a = AnyHashable(A())
let desc4: [AnyHashable: String] = ["🥴" : "emoji", 42 : "an Int", a : "an A()"];

また自身もHashableプロトコルに準拠する。 つまりAnyHashableは、Hashableプロトコルに準拠している型である ということを宣言している。

AnySequence型

型消去ラッパー型の使い方は、異なる型を同一の型に変換するだけではない。 「あるコンテキストを持つ型 を受け取れる」という宣言としても使用することが出来る。

これはSwiftが Selfassociatedtype を持つプロトコルは型宣言として利用できないための代替である。

// NG
func test(seq: Sequence) { ...

// 型制約としてはOK
func test<T>(seq: T) where T: Sequence, T.Element == Character { ...

例えば、Characterのシーケンスを受け取り、あるトークンを返すという関数を考える。

// Array型として引数を受け取る
func convertToken(from chars: Array<Character>) -> Token { ...

このとき関数を以下のように呼び出すと、呼び出すたびにArrayを生成するためO(N)のコストを必要とする

// 一度Arrayを生成する必要がある。
convertToken(from: Array("Hello World"));

そこでAnySequeceを使うことで、コストなく引き渡すことが出来るようになる。

func convertToken(from chars: AnySequence<Character>) -> Token { ... 
// Stringから呼び出しても生成コスト0
convertToken(from: AnySequence("Hello"))

// Array<Character>から呼び出しても生成コスト0
convertToken(from: AnySequence(Array("Hello")))

これは以下のようにジェネリクスを使うよりもリーダブルにもなる。

func convertToken<T>(from chars: T) -> Token where T: Sequence, T.Element == Character { ...

まとめ

型消去の使い方

  • 異なる型を同一の型に変換できる
  • Selfassociatedtype を持つProtocolの代わりに型宣言として使用できる