count(where:)
01 何が問題だったのか
Swift の Sequence には map や filter といった便利なメソッドが備わっていますが、「条件を満たす要素の個数を数える」だけの操作は標準では提供されていませんでした。数えたいだけなのに、書き方ごとに何らかの不満が残る状態になっていました。
素直に思いつくのは filter と count の組み合わせです。
[1, 2, 3, -1, -2].filter({ $0 > 0 }).count // => 3
読みやすい一方で、filter が中間配列を生成してすぐ捨てるため、要素数を数えるだけの目的には無駄があります。
中間配列を避けるには lazy を挟む方法があります。
[1, 2, 3, -1, -2].lazy.filter({ $0 > 0 }).count // => 3
ただし lazy の filter はクロージャを @escaping として受け取るため、キャプチャなどの扱いが変わってきます。
もう一つの選択肢は reduce で直接書き下す方法です。
[1, 2, 3, -1, -2].reduce(0) { $1 > 0 ? $0 + 1 : $0 }
性能面では悪くありませんが、コードの意図が数えることではなく畳み込みの実装詳細に埋もれてしまい、読み手の負担が大きくなります。
このように、「条件を満たす要素を数える」という単純な操作に対して、素朴で効率のよい書き方が標準ライブラリに用意されていない、というのがこの proposal の問題意識です。
02 どのように解決されるのか
Sequence に count(where:) メソッドが追加されました。述語クロージャを渡すと、それを満たす要素の個数を Int で返します。
[1, 2, 3, -1, -2].count(where: { $0 > 0 }) // => 3
中間配列を作らずに一度のパスで数え上げるため、filter(_:).count のような無駄がありません。また lazy を挟む必要がないので、クロージャは通常の non-escaping として書けます。
シグネチャ
標準ライブラリには次のような定義で追加されます。
extension Sequence {
public func count(
where predicate: (Element) throws -> Bool
) rethrows -> Int {
var count = 0
for element in self {
if try predicate(element) {
count += 1
}
}
return count
}
}
ポイントは次のとおりです。
Sequenceに対して定義されているため、ArrayだけでなくSetやDictionary、独自のSequenceでも同じように使えます。- クロージャは
throws可能で、メソッド自体はrethrowsなので、投げないクロージャを渡せば呼び出し側もtryを書く必要はありません。
使用例
Sequence 全般で使えるので、たとえば Dictionary に対して「特定の条件を満たすエントリの数」を直接数えられます。
let scores: [String: Int] = ["A": 80, "B": 55, "C": 92, "D": 40]
let passed = scores.count(where: { $0.value >= 60 }) // => 2
文字列の要素(Character)を数えることもできます。
let s = "Hello, world!"
let letters = s.count(where: { $0.isLetter }) // => 10
filter(_:).count と同じ結果を、より意図が明確でパフォーマンスにも優れる形で書けるようになります。
歴史的経緯についての補足
count(where:) は一度 Swift 5.0 に向けて受理されましたが、当時の型チェッカの事情で式の型推論を重くするケースが見つかったため、いったん差し戻されていました。その後、型チェッカの改善と再レビューを経て、Swift 6.0 で改めて標準ライブラリに取り込まれています。