Eliminate IndexDistance from Collection
01 何が問題だったのか
Collection プロトコルには、2つのインデックス間の距離を表す関連型として IndexDistance がありました。IndexDistance は SignedInteger に適合する任意の整数型にできる設計で、標準ライブラリでも多くのAPIで使われていました。
protocol Collection {
associatedtype IndexDistance: SignedInteger
var count: IndexDistance { get }
func index(_ i: Index, offsetBy n: IndexDistance) -> Index
func distance(from start: Index, to end: Index) -> IndexDistance
// ...
}
ジェネリックなコレクション処理を書く上での大きな障害になっていた
実際の利用現場では、インデックス間の距離はほぼ常に Int です。それでも関連型として抽象化されているために、Collection を使ったジェネリックなアルゴリズムを書こうとすると、次のような手間が必要になっていました。
- リテラルに明示的な型注釈が要る。たとえば
let i = 0ではなくlet i: IndexDistance = 0と書かないと型が決まらない - 距離型の違うコレクション同士をまたぐと
numericCastでの変換が必要になる - その
numericCastが正しい向きの変換になっているか(大きな型から小さな型への変換で溢れないか)を人間が判断しなければならない
結果として、コレクションに対するジェネリックな処理を書くという問題が、同時に数値型の変換問題にもなってしまっていました。Swift 4.0 で関連型への where 制約(SE-0142)が入り、再帰的なプロトコル制約(SE-0157)の対応も進んで、中級者でもジェネリックなアルゴリズムを書けるようになりつつあったなかで、IndexDistance はその最後の、そして最大の障害として残っていました。
実質的に Int を前提にしたコードが溢れていた
こうした事情から、標準ライブラリの外でも「どうせ Int だろう」として where IndexDistance == Int を付けてジェネリック性を諦めるコードが多く、さらには Index == Int まで固めてしまうコードも珍しくありませんでした。標準ライブラリ内部のジェネリックコードも、Int.max を超えるサイズのコレクションを渡されれば実際にはトラップする作りになっているものが多く、「Int 以外の距離型を本気で想定したコード」はほとんど存在しないのが実情でした。
また、IndexDistance がジェネリックに残っていることはコンパイラの最適化(特殊化)にも不利で、コンパイル時間を押し上げる要因にもなっていました。
Swift 公式のガイダンス自体、特別な理由がない限り整数型には Int を使うよう勧めています。IndexDistance の存在はこの指針とも噛み合っておらず、数十億要素を超える巨大コレクションや、Int より小さい距離型で省メモリ化したい極端に小さなコレクションといったニッチなユースケースのためだけに、すべてのジェネリックコードに恒常的なコストを課している状態でした。
02 どのように解決されるのか
Collection から関連型 IndexDistance を削除し、距離を表す型をすべて Int に固定します。標準ライブラリ内で IndexDistance を使っていたAPIは、署名がそのまま Int に置き換わります。
protocol Collection {
var count: Int { get }
func index(_ i: Index, offsetBy n: Int) -> Index
func index(_ i: Index, offsetBy n: Int, limitedBy limit: Index) -> Index?
func distance(from start: Index, to end: Index) -> Int
// ...
}
これにより、ジェネリックなコレクション処理の中でインデックス距離を扱うときに、明示的な型注釈や numericCast が要らなくなります。
// 変更前: IndexDistance を意識して書く必要があった
func dropFirstHalf<C: Collection>(of c: C) -> C.SubSequence {
let half: C.IndexDistance = numericCast(c.count / 2)
return c.dropFirst(numericCast(half))
}
// 変更後: Int をそのまま扱える
func dropFirstHalf<C: Collection>(of c: C) -> C.SubSequence {
return c.dropFirst(c.count / 2)
}
標準ライブラリの具体的な変更
IndexDistance を使っていた箇所はすべて Int に置き換わります。標準ライブラリで IndexDistance を Int 以外に定義していた唯一の型は AnyCollection で、そこでは Int64 を使っていましたが、これも Int に揃えられます。
既存コードへの移行
既存のジェネリックアルゴリズムのうち、
where IndexDistance == Intのような制約を付けていたもの- メソッド本体で
IndexDistanceを型として参照していたもの
については、Collection の拡張に IndexDistance を Int のdeprecated typealiasとして残すことで、ソース互換を保ったまま移行できるようにします。警告は出ますが、ほとんどのコードはそのままコンパイルが通ります。
一方、IndexDistance を Int 以外の型として定義しているコレクションを自作している場合は、本当にソース非互換な変更になります。ただし、Swift のソース互換性スイートに含まれるコードのなかにそうした型は存在しておらず、実際に影響を受けるケースは極めて稀であるという前提に立っています。