Generic Subscripts
01 何が問題だったのか
Swiftではメソッドやイニシャライザをジェネリックに書けますが、subscript(添字)だけはジェネリックにできませんでした。そのため、型パラメータを取ることが自然な添字アクセスを、subscript として素直に表現できないという制約がありました。
例えば Collection に対して、複数のインデックスをまとめて渡して対応する要素列を取り出したい場合、ジェネリックメソッドとしてなら書けますが、添字の形では書けません。同じく JSON のような型で、キーに対して取り出したい型を呼び出し側で指定する API を作りたくても、添字としては実現できませんでした。
この制約には具体的な不都合があります。
- 本来ジェネリックにすれば汎用的に使える添字が、特定の型に固定するしかなく、使い勝手が悪いままでした。
- 意味的には添字として書くのが自然な API を、関数として書かざるを得ませんでした。関数として書くと代入構文(lvalue)として使えないため、読み取りはともかく書き込みを伴う用途では構文が不格好になります。
また、標準ライブラリ側でも、シーケンスの末尾操作名の整理(SE-0132)や String まわりの設計で、ジェネリックな添字があれば素直に表現できる API がいくつか存在しており、基盤として欠けているピースになっていました。
加えて、subscript は関数と文法的によく似ているにもかかわらず、デフォルト引数を持てないという差異もあり、関数と添字でコンパイラの扱いが不必要に分かれていました。
02 どのように解決されるのか
subscript にジェネリックパラメータリストと where 節を書けるようにします。構文はメソッドと揃え、subscript キーワードの直後に <...> を、パラメータリストと戻り値型の後ろに where 節を置きます。あわせて、subscript にもデフォルト引数を書けるようにし、関数との扱いを統一します。
複数インデックスをまとめて取り出す
Collection に、インデックスの並びを受け取って対応する要素を配列で返す添字を、ジェネリックに定義できます。
extension Collection {
subscript<Indices: Sequence>(indices: Indices) -> [Iterator.Element]
where Indices.Iterator.Element == Index {
// indices を走査して self[index] を集める
var result: [Iterator.Element] = []
for index in indices {
result.append(self[index])
}
return result
}
}
let letters = ["a", "b", "c", "d", "e"]
let picked = letters[[0, 2, 4]]
// picked == ["a", "c", "e"]
インデックス列の具体的な型(配列、範囲、遅延シーケンスなど)に縛られず、Index を要素とする任意の Sequence を受け付けられます。
戻り値をジェネリックにする
戻り値側に型パラメータを置くこともできます。呼び出し側の文脈(代入先の型など)から型を推論させて、同じキーに対して異なる型で値を取り出せます。
protocol JSONConvertible { /* ... */ }
extension JSON {
subscript<T: JSONConvertible>(key: String) -> T? {
// key に対応する値を T として取り出す
// ...
}
}
let json: JSON = /* ... */
let name: String? = json["name"]
let age: Int? = json["age"]
関数ではなく添字として書けるようになったことで、json["name"] = "Alice" のような代入構文(lvalue としての利用)も素直に書けます。
デフォルト引数
subscript のパラメータにもデフォルト値を指定できます。関数と同じ書き方です。
extension Container {
subscript<A>(index: A? = nil) -> Element {
// ...
}
}
これにより、関数と subscript でコンパイラの処理が共通化され、API の設計時に「関数なら書けるが添字では書けない」という差が減ります。