Change RangeReplaceableCollection.filter to return Self
01 何が問題だったのか
Swift 標準ライブラリの filter は、もともと Sequence のメソッドとして定義されており、戻り値は常に [Element](配列)でした。たとえば String は Character のシーケンスですから、次のように書くと結果は String ではなく [Character] になります。
let filtered = "abcd".filter { $0 == "a" }
// filtered は [Character]
同じ値からもう一度 String を組み立て直したいことが多く、そのたびに String(filtered) のような変換が必要でした。配列を中間生成する分、パフォーマンス上も無駄があります。
先行する SE-0165(Dictionary & Set Enhancements)では、Dictionary.filter の戻り値を Dictionary に変えることで、この使い勝手とパフォーマンスの問題を解決しました。しかし結果として、
DictionaryだけはfilterがSelfを返す一方、Stringのような他の「要素を足して自分自身を組み立て直せるコレクション」では依然として[Element]が返る- ジェネリックなコードの中で
Sequenceとして受け取ってfilterすると、せっかくのDictionaryでも結果は配列に戻ってしまう
というように、挙動が一貫しないという新たな問題が生まれていました。本来は、自分自身と同じ型で結果を組み立て直せるコレクションなら、filter も同じ型を返すほうが自然です。
02 どのように解決されるのか
RangeReplaceableCollection に filter のデフォルト実装を追加し、戻り値を Self にします。RangeReplaceableCollection は空のインスタンスを作って要素を append していけるプロトコルなので、フィルタ結果を同じ型として素直に組み立て直せます。
実装は次のように、空の Self を用意して条件に合う要素だけを追加していくシンプルな形です。
extension RangeReplaceableCollection {
func filter(_ isIncluded: (Iterator.Element) throws -> Bool) rethrows -> Self {
var result = Self()
for element in self {
if try isIncluded(element) {
result.append(element)
}
}
return result
}
}
これにより、String や Array、Data など RangeReplaceableCollection に適合する型では、filter の結果がそのまま自身の型になります。SE-0163 で String が RangeReplaceableCollection に適合したため、String もこの恩恵を受けます。
let filtered = "abcd".filter { $0 == "a" }
// filtered は String("a")
Dictionary.filter が Dictionary を返す挙動とも足並みが揃い、ジェネリックな文脈でもコレクション種別に応じた自然な戻り値が得られるようになります。
一方で、stride や Range のように「自分自身をフィルタした結果を Self として表現できない」シーケンスは、この改善の対象外です。これらは引き続き Sequence 側の filter が使われ、戻り値は [Element] のままになります。パフォーマンスが気になる場合は lazy と組み合わせるのが引き続き有効です。
既存コードへの影響
この変更は見た目には小さいものの、結果の型が変わるため、型推論に依存した一部のコードは影響を受けます。たとえば、以下のように型を明示せずに変数で受けてから配列を要求する API に渡す書き方は、変更後にコンパイルが通らなくなります。
// 以前は [Character]、変更後は String
let filtered = "abcd".filter { $0 == "a" }
useArray(filtered) // won't compile
そのため、新しい RangeReplaceableCollection.filter は Swift 4 以降でのみ有効化されます。どうしても従来どおりの配列が欲しい場合は、型コンテキストで明示できます(Sequence 側の filter が呼ばれます)。
let filtered = "abc".filter(predicate) as [Character]
map への拡張は今回対象外
同様の改善を map にも広げたくなりますが、map はフィルタと違って要素の型が変わり得るため、「要素の型だけが違う Self」を型レベルで表現する必要があります。現在のジェネリクスの表現力ではそれが書けないため、今回のスコープからは外れています。