AnySequenceってなんなの?
AnySequenceってなんなの?っていうのを調べようとしたメモです。
JavaのGenericsは本当に型情報を消し去るイレイジャなのでまず意識しなくていいし、C#(4.0以上)のGenericsは共変性・反変性を持っているので、Swiftだとなんでややこしいコードになるのかよくわからなかったのです。
まず、SequenceTypeプロトコルに準拠した独自のクラスを作ることを考えます。
// 本
struct Book {
var name: String
}
// 本棚
final class BookShelf: SequenceType {
private var books: [Book] = []
func append(book: Book) {
self.books.append(book)
}
// Swift 3.0 だと AnyIterator とかいうストレートな名前になるはず
func generate() -> AnyGenerator<Book> {
var index : Int = 0
return AnyGenerator<Book> {
// index++ を Swift 3.0 準拠で書くとこうなる、微妙…
defer {
index = index.successor()
}
return index < self.books.count ? self.books[index] : .None
}
}
}
SequenceTypeに準拠することで、自作のクラスでもfor-in構文を利用できます。
let bookShelf = BookShelf()
bookShelf.append(Book(name: "ハイペリオン"))
bookShelf.append(Book(name: "ハイペリオンの没落"))
bookShelf.append(Book(name: "エンディミオン"))
bookShelf.append(Book(name: "エンディミオンの覚醒"))
for book in bookShelf {
print(book.name)
}
ここまでは簡単です。
問題は、「BookShelfという実装詳細を隠したい場合はどうすればいいか?」と考えたときです。SequenceTypeプロトコルとして扱うという素直なアプローチは使えません。
let sequence: SequenceType = bookShelf
以下のコンパイルエラーになります。
error: protocol 'SequenceType' can only be used as a generic constraint because it has Self or associated type requirements
これがなぜかというと、SequenceTypeの定義を見ると分かるように、
public protocol SequenceType {
associatedtype Generator : GeneratorType
associatedtype SubSequence
// 略
SequenceTypeは付属型(associatedtypeになって意味がわかりやすくなりましたね!)を持っているためです。Swiftの仕様上、付属型を持つプロトコルはgenericの制約としてしか使えません。
GeneratorTypeとSubSequenceを具体化したクラスが必要です。そこで、AnySequenceの出番です。
let sequence: AnySequence<Book> = AnySequence(bookShelf)
for book in sequence {
print(book.name)
}
AnySequenceでラップすることで、BookShelfの実装詳細を隠蔽することができました。
ただし同じElementの型でまとめるのが限界です。AnySequence<AnyObject>にAnySequence<Book>を突っ込むみたいな融通は効かない(共変性とか反変性みたいなのはない)です。
ポケモン型消去
この辺の説明をポケモンを使ってすっごいわかりやすく説明したのが、ポケモン型消去の話です。
以下、Sequenceよりもポケモンの方が分かりやすそうなので、ポケモンで考えます。
この説明ですが、初見ではAnyPokemonがattackの実装をイニシャライザで退避させている理由がわかりませんでした。
class AnyPokemon <PokemonType>: Pokemon {
private let _attack: ((PokemonType) -> Void)
required init<U:Pokemon where U.PokemonType == PokemonType>(_ pokemon: U) {
_attack = pokemon.attack
}
func attack(type:PokemonType) {
return _attack(type)
}
}
これは実際に手を動かせば答えはすぐに出ました。PokemonTypeはAnyPokemonに渡すポケモンの型に束縛される―たとえばイニシャライザにAnyPokemon<Electric>のピカチュウを渡して初めて、attackの型がattack(type :Electric) -> ()に決定される―からです。
先の説明の通り、付随型を持つプロトコルのままポケモンを保持することはできないので、束縛された後の実装を保持するようにしているのです。でもプロトコルのすべての実装を、イニシャライザで退避させるようなAnyクラスを作るのって正直エレガントだと思えません。
そこで、AnySequenceがどうやってSequenceTypeの各処理を移譲しているのか調べようと考えたのです。
AnySequenceは言語仕様内で型安全と型消去を両立している
下調べしたとき、Swift 2.1のバグにちょうどそんなのがあったのでちょろいと思ったのですよ。
AnySequenceのイニシャライザでは、元のシーケンスをさらに_SequenceBoxにラップして処理を移譲していたのですが、S.Generator.ElementとS.SubSequence.Generator.Elementとの型が別になるような、変なGeneratorを実装した場合に、移譲がうまくいかないのでイニシャライザに制約を加えたとのこと。
だから、_SequenceBoxの実装を探して終わりだと思ったんですけど、これは自動生成されているボイラーコードなので単純には見つかりませんでした。
自動生成されているコードを、ポケモンの例に置き換えるとたぶんこんな感じになるのだと思います。
繰り返しますがSwiftでは付随型を持つPokemonプロトコルを型として持てません。
そこでまず付随型をジェネリクスにした_AnyPokemonBaseという抽象クラスを作り、型として保持できるようにします。もちろん、Swiftの言語仕様に抽象クラスがあるわけではないので、空実装を提供した「抽象クラスっぽい何か」です。
そしてその_AnyPokemonBaseを継承した_AnyPokemonBoxに実際のポケモンをラップします。実際のPokemonプロトコルの処理はここから移譲します。
2段階ラップして、抽象クラス経由で操作することで、型消去したい型を保持する(日本語が迷走してる)、というようなことを実現しています。Pokemonプロトコルに機能が増えた場合にはボイラーコードを書くことになることには変わりありませんし(StdLibはこの辺は自動化している)、素直にabstractなクラスが言語仕様にあった方が幸せな気がするのですが、仕組みとしてはうまいと思いました。
AnySequenceの場合、内部にAnyGeneratorを持っているのでより複雑ですが、ほぼ同じ構造です。きっと。
なお、以下の記事を大変参考にしました。
ほぼ真似と言っても過言ではありません。こんなの自力で辿り着けないです…。