Constraining AnySequence.init
01 何が問題だったのか
AnySequence は、任意のシーケンスを同じ型として扱えるようにするための型消去ラッパーです。内部では元のシーケンス(ベースシーケンス)を抱え込んで、AnySequence に対するメソッド呼び出しをベース側に委譲することで動きます。
しかし Swift 2.x 当時の AnySequence は、SequenceType(現在の Sequence に相当)のメソッド呼び出しをベースシーケンスに委譲していませんでした。この委譲が無いことで、二つの問題が生じていました。
動的ダウンキャストが避けられない
SequenceType.dropFirst や SequenceType.prefix など、SubSequence を返すメソッドのデフォルト実装は、効率のために実装内部で動的ダウンキャストを使って最適なパスを選ぼうとします。AnySequence がベースに委譲しないと、こうしたデフォルト実装の中で余計なダウンキャストが走ってしまい、型消去の裏側で不要なコストが発生していました。
カスタム実装が無視される
さらに深刻なのは、ベースシーケンスが SequenceType のメソッドを独自に最適化して実装していても、AnySequence 経由で呼ぶとそのカスタム実装が呼ばれないことです。AnySequence で包んだ瞬間にデフォルト実装にフォールバックしてしまうため、せっかく用意した高速なパスが使われなくなっていました。
なぜ委譲できなかったのか
委譲を実装するには、AnySequence が内部で保持するボックス(_SequenceBox)が、ベースシーケンスだけでなくその SubSequence も一緒に扱える必要があります。しかし当時のジェネリクスでは、_SequenceBox の型パラメータに SubSequence の性質に関する制約を十分に書けなかったため、委譲を成立させるのに必要な形で AnySequence.init を定義できませんでした。
// 変更前: SubSequence についての制約が無く、委譲を組み立てられない
public struct AnySequence<Element> : SequenceType {
public init<
S: SequenceType
where
S.Generator.Element == Element
>(_ base: S) { ... }
}
02 どのように解決されるのか
AnySequence.init に、ベースシーケンスの SubSequence に関する追加の制約を課します。この制約を通じて内部ボックスがベースシーケンスと SubSequence を揃って扱えるようになり、AnySequence から各メソッド呼び出しをベースシーケンスに委譲できるようになります。
新しい制約
AnySequence.init には次の3つの制約が追加されます。
// 変更後
public struct AnySequence<Element> : SequenceType {
public init<
S: SequenceType
where
S.Generator.Element == Element,
S.SubSequence : SequenceType,
S.SubSequence.Generator.Element == Element,
S.SubSequence.SubSequence == S.SubSequence
>(_ base: S) { ... }
}
意味としては、
SubSequence自体がSequenceTypeに適合していることSubSequenceの要素型がベースシーケンスと一致することSubSequenceをさらにスライスした型も同じSubSequenceであること
の3点です。これらは本来 SequenceType プロトコル自身に書きたい制約ですが、当時のSwiftのジェネリクスでは表現できなかったため、AnySequence.init 側で個別に要求する形になっています。
ダウンキャスト除去とカスタム実装の尊重
この委譲が成立したことで、AnySequence を経由したメソッド呼び出しは、SequenceType のデフォルト実装に頼らず、ベースシーケンスの実装をそのまま呼び出せるようになりました。結果として、
- デフォルト実装の中で走っていた余計な動的ダウンキャストが不要になる
- ベースシーケンスが提供するカスタム実装(最適化された
dropFirstなど)がAnySequence越しでもそのまま活きる
という二つの改善が同時に得られます。
既存のシーケンス型への影響
標準ライブラリが提供するシーケンス型は、もともと SubSequence.SubSequence == SubSequence を満たす形で作られているため、今回の追加制約の影響を受けません。サードパーティのコレクションでも、デフォルトの SubSequence(当時の Slice)をそのまま使っているものは問題なく通ります。独自の SubSequence を定義しているコレクションのうち、SubSequence.SubSequence == SubSequence を満たさないものだけは、AnySequence で包む際にコンパイルエラーとなります。