Swift Digest
SE-0174 | Swift Evolution

Change RangeReplaceableCollection.filter to return Self

Proposal
SE-0174
Authors
Ben Cohen
Review Manager
Doug Gregor
Status
Implemented (Swift 4.2)

01 何が問題だったのか

Swift 標準ライブラリの filter は、もともと Sequence のメソッドとして定義されており、戻り値は常に [Element](配列)でした。たとえば StringCharacter のシーケンスですから、次のように書くと結果は String ではなく [Character] になります。

let filtered = "abcd".filter { $0 == "a" }
// filtered は [Character]

同じ値からもう一度 String を組み立て直したいことが多く、そのたびに String(filtered) のような変換が必要でした。配列を中間生成する分、パフォーマンス上も無駄があります。

先行する SE-0165(Dictionary & Set Enhancements)では、Dictionary.filter の戻り値を Dictionary に変えることで、この使い勝手とパフォーマンスの問題を解決しました。しかし結果として、

  • Dictionary だけは filterSelf を返す一方、String のような他の「要素を足して自分自身を組み立て直せるコレクション」では依然として [Element] が返る
  • ジェネリックなコードの中で Sequence として受け取って filter すると、せっかくの Dictionary でも結果は配列に戻ってしまう

というように、挙動が一貫しないという新たな問題が生まれていました。本来は、自分自身と同じ型で結果を組み立て直せるコレクションなら、filter も同じ型を返すほうが自然です。

02 どのように解決されるのか

RangeReplaceableCollectionfilter のデフォルト実装を追加し、戻り値を 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
    }
}

これにより、StringArrayData など RangeReplaceableCollection に適合する型では、filter の結果がそのまま自身の型になります。SE-0163 で StringRangeReplaceableCollection に適合したため、String もこの恩恵を受けます。

let filtered = "abcd".filter { $0 == "a" }
// filtered は String("a")

Dictionary.filterDictionary を返す挙動とも足並みが揃い、ジェネリックな文脈でもコレクション種別に応じた自然な戻り値が得られるようになります。

一方で、strideRange のように「自分自身をフィルタした結果を 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」を型レベルで表現する必要があります。現在のジェネリクスの表現力ではそれが書けないため、今回のスコープからは外れています。