Add an allSatisfy algorithm to Sequence
01 何が問題だったのか
シーケンスのすべての要素が特定の条件を満たすかを確認したい場面は頻繁にあります。たとえば「すべての要素が 9 か」「すべての要素が奇数か」といった判定です。
Swift 4.1 までの標準ライブラリには、この用途に直接対応するメソッドがありませんでした。そのため利用者は、述語の真偽と結果の両方を反転させる形で contains(where:) を使う必要がありました。
// すべての要素が 9
!nums.contains { $0 != 9 }
// すべての要素が奇数
!nums.contains { !isOdd($0) }
このコードは、二重否定を読み解く必要があり、可読性を大きく損ないます。また、contains をこのように使えること自体に気付かない利用者が、自前で for ループを書いたり、次のような非効率な実装に流れてしまうケースもありました。
// 早期終了できず、最後まで走ってしまう
nums.reduce(true) { $0.0 && $0.1 == 9 }
// 集合化して個数と先頭要素を見る、という遠回りな書き方
Set(nums).count == 1 && Set(nums).first == 9
手書きの for ループは単純なバグを生みやすく、reduce を使った実装は条件が満たされなくなった時点で処理を打ち切ることができないため、パフォーマンス上も不利です。要するに、「すべての要素が条件を満たすか」というごく基本的な問いに対して、可読性と効率の両方を満たす標準的な書き方が欠けていたのが問題でした。
02 どのように解決されるのか
Sequence に allSatisfy(_:) メソッドが追加されました。与えた述語をすべての要素が満たすときに true を返すメソッドで、シグネチャは次のとおりです。
extension Sequence {
/// Returns a Boolean value indicating whether every element of the sequence
/// satisfies the given predicate.
func allSatisfy(_ predicate: (Element) throws -> Bool) rethrows -> Bool
}
これにより、先ほどの二重否定や reduce による書き方を、意図がそのまま表れる形に書き換えられます。
// すべての要素が奇数
nums.allSatisfy(isOdd)
// すべての要素が 9
nums.allSatisfy { $0 == 9 }
contains(where:) と同様に、述語が false を返した時点で走査は打ち切られるため、早期終了によるパフォーマンス上の利点も得られます。空のシーケンスに対しては true を返します(数学的な「すべての〜について成り立つ」に対応する自然な挙動です)。述語は throws を付けられ、呼び出し側は rethrows を通じて必要なときだけ try を書けば済みます。
命名について
レビューでは containsOnly や all といった候補も検討されました。all は他言語で前例がある一方、トレイリングクロージャでは引数ラベルが省略されるため呼び出し箇所で意図が伝わりにくく、containsOnly は空のシーケンスに対する挙動が直感と合いにくい(「これだけを含む」と読めてしまう)という理由で見送られ、最終的に allSatisfy が採用されています。