One-sided Ranges
01 何が問題だったのか
コレクションの一部を切り出すとき、先頭から特定のインデックスまで、あるいはあるインデックスから末尾まで、というスライスを取りたい場面が頻繁にあります。Swift 3 まではこのような片側だけが指定されるスライスを、範囲演算子と startIndex / endIndex を組み合わせて書く必要がありました。
let s = "Hello, World!"
let i = s.index(of: ",")!
let greeting = s[s.startIndex..<i]
このように、s.startIndex や s.endIndex を何度も書くのは冗長で、読むときも「どちらの端からどこまでなのか」がパッと把握しづらいという問題があります。
Swift 3 には代替として、インデックスを受け取る prefix(upTo:) / prefix(through:) / suffix(from:) というメソッド群も用意されていました。
let greeting = s.prefix(upTo: i)
let withComma = s.prefix(through: i)
let location = s.suffix(from: i)
しかし、これらは範囲演算子を使う書き方とは見た目がまったく異なり、「似た操作なのに書き方が二種類ある」という一貫性のなさを生んでいました。また、メソッドの戻り値はl-valueとして使えないため、スライスに対して代入する(部分置換する)ような用途では subscript 側を使わざるを得ない、という非対称性もありました。
加えて、範囲を引数に取るAPIのオーバーロードも問題を抱えていました。Range 以外にも ClosedRange、CountableRange、CountableClosedRange など複数の範囲型があり、それぞれを受け付けるためには同じメソッドを何度もオーバーロードする必要があったためです。範囲を表現する型が増えるほど、この負担は大きくなります。
i... 相当の無限シーケンスも欲しい
実用面では、特定の整数から始まって増え続ける無限シーケンスが欲しくなる場面もあります。たとえば Sequence.enumerated() は常に 0 起点ですが、「1 から番号を振りたい」「逆に要素の方を左に置きたい」といった場合に、開始位置と向きを自由に選べる手段が標準ライブラリにはありませんでした。
02 どのように解決されるのか
範囲演算子 ..< と ... に、片側だけを省略した one-sided range の構文が追加されます。省略された側は、使われる文脈に応じて開始位置または終了位置に推論されます。
let s = "Hello, World!"
let i = s.index(of: ",")!
// 先頭から i の手前まで(half-open)
let greeting = s[..<i]
// 先頭から i まで含める(closed)
let withComma = s[...i]
// i から末尾まで
let location = s[i...]
s.startIndex や s.endIndex を書く必要がなくなり、「どちら側が省略されているか」が演算子の位置から一目で分かります。左側を省略するときは ..< と ... の両方が使えますが、右側を省略するとき(i...)は終端の扱いに違いが生じないため、... の一種類だけが用意されます。
prefix / suffix との関係
この新構文は、インデックスを受け取っていた prefix(upTo:) / prefix(through:) / suffix(from:) を置き換えるものとして位置付けられます。これらのメソッドは Swift 4 で deprecated になり、将来のバージョンで削除される予定です。なお、距離(個数)を受け取る prefix(_:) / suffix(_:) はこの提案の対象外で、そのまま残ります。
また、subscript として書けるようになることで、部分置換もそのまま書けます。
var a = [1, 2, 3, 4, 5]
a[..<2] = [10, 20, 30]
// a == [10, 20, 30, 3, 4, 5]
i... はシーケンスとしても使える
インデックスの型がカウント可能な整数などの場合、i... は i から増え続ける無限シーケンスとして振る舞います。これを使うと、zip と組み合わせて番号付けの開始位置や順序を自由に選べます。
// 1 から番号を振る
for (n, c) in zip(1..., "abc") {
print(n, c)
}
// 1 a
// 2 b
// 3 c
// 要素の方を左に置いて番号を右に
for (c, n) in zip("abc", 0...) {
print(c, n)
}
無限シーケンスなので、インデックス型の上限(たとえば Int なら Int.max)を超えて反復した場合の挙動はインデックス型に委ねられます。Int の場合はオーバーフロー時に実行時エラーになります。
switch のパターンとしても使える
~= の実装も汎用化されるため、one-sided range を switch の case にそのまま書けます。
switch i {
case 9001...:
print("It's over NINE THOUSAAAAAAAND")
default:
print("There's no way that can be right!")
}
RangeExpression プロトコルの導入
実装の裏側では、Range / ClosedRange / PartialRangeUpTo / PartialRangeThrough / PartialRangeFrom といったすべての範囲型が、新しい RangeExpression プロトコルに適合します。
public protocol RangeExpression {
associatedtype Bound: Comparable
func relative<C: Collection>(to collection: C) -> Range<Bound>
where C.Index == Bound
func contains(_ element: Bound) -> Bool
}
relative(to:) は、部分範囲を特定のコレクションに対する具体的な Range<Index> に変換するためのメソッドです。これにより、コレクション側は範囲の種類ごとにオーバーロードを用意する必要がなくなり、RangeExpression を受け取る1つのジェネリックな subscript で済みます。
extension Collection {
public subscript<R: RangeExpression>(r: R) -> SubSequence
where R.Bound == Index { get }
}
利用者として RangeExpression を直接使ったり新しい範囲型を自作したりする場面は通常ありませんが、範囲を引数に取るAPIを書くときに「どの範囲型でも受けられる」ようにできるのが利点です。