Removing Where Clauses from For-In Loops
01 何が問題だったのか
Swift の for-in ループには、要素ごとに条件を与えて絞り込むための where 節が用意されていました。たとえば次のように書くと、theArray の奇数の要素だけを print できます。
var theArray = [1, 2, 3, 4, 5, 6, 7, 8, 9]
for x in theArray where x % 2 == 1 {
print(x)
}
しかし、この where 節には次のような問題点が指摘されていました。
使用頻度が低く、発見されにくい
Swift 標準ライブラリ内での for-in-where の使用例は約 600 件の for-in に対してわずか 3 件、GitHub 上の主要な Swift リポジトリを調査しても 650 件以上の for-in に対して for-in-where は 1 件しか見つからないなど、実際にはほとんど使われていませんでした。構文糖衣として用意されているにもかかわらず、多くの利用者が存在に気づいていないか、使う機会を見いだせていない状態でした。
ひとつのスタイル(フィルタ)だけを優遇してしまう
for-in の where 節は、条件を満たさない要素に対して continue するという「フィルタ」の動作しか表現できません。一方、ループ内で条件に応じて分岐したい場面では、break したい/return したい/throw したい/fatalError() したい、といった複数のスタイルがあり得ます。where はその中のひとつ(フィルタ)だけを特別扱いする形になっていました。
while の where と意味が揃っていなかった
当時は while 文にも where 節を書けましたが、for-in の where が条件を満たさない要素を continue でスキップするのに対し、while の where は条件を満たさなくなった時点で break する、という異なる意味を持っていました。
var theArray = [1, 2, 3, 4, 5, 6, 7, 8, 9]
for x in theArray where x % 2 == 1 { print(x) } // 偶数を continue(スキップ)
var anArray = [1, 2, 3, 4, 5, 6, 7, 8, 9]
while let x = anArray.popLast() where x % 2 == 1 { print(x) } // 偶数で break(抜ける)
同じ where というキーワードが、似た見た目でありながら文脈によって「continue」か「break」かが変わるため、特に新しい利用者にとって混乱の原因になっていました。
デバッグやコメントがしづらい
where 節は流れるような記述を可能にする一方、条件部分に個別にブレークポイントを張ったり、コメントを添えたりするのが難しく、guard のような文として書いた場合と比べてデバッグや文書化の面で不利でした。
文法上の不整合
switch の case や catch 節ではパターンの直後に where 節が置かれるのに対し、for-in の where 節はパターンではなくシーケンス式の後ろに置かれ、修飾対象(パターンなのかシーケンスなのか)が見た目からわかりにくくなっていました。本来であれば for case? pattern where-clause? in expression のような並びが自然で、for-in の where は Swift の文法の中で一貫性を欠いた存在でした。
関連する変更(SE-0099)
同時期に採択された SE-0099 では、if や while などの条件節から where キーワードを引退させ、真偽条件の導入子としての where を廃止することが決まりました。for-in の where 節だけを残すと、言語全体としての一貫性がさらに損なわれることになります。
02 どのように解決されるのか
この Proposal は、for-in ループの文法から where 節を取り除くことを提案しました。文法は次のように単純化されます。
for case? pattern in expression code-block
これまで where 節で表現していたフィルタは、ループ本体の先頭に guard(必要に応じて if)を書いてリファクタリングします。
// 変更前
for x in theArray where x % 2 == 1 {
print(x)
}
// 変更後
for x in theArray {
guard x % 2 == 1 else { continue }
print(x)
}
guard を使うことで、これまで where に詰め込まれていた「条件を満たさない場合どうするか」を、スタイルに応じて自由に選べるようになります。
for x in sequence {
guard condition else { continue } // 従来の where の挙動
guard condition else { break } // 条件を満たさなくなったらループを抜ける
guard condition else { return } // 関数から抜ける
guard condition else { throw error } // エラーを投げる
guard condition else { fatalError() } // 異常終了させる
// ...
}
同じ for-in のパターンの中でも、continue / break / return / throw / fatalError() といった選択肢を明示的に書き分けられるため、意図が読み手に伝わりやすく、個々の条件にコメントやブレークポイントを添えることも容易になります。
この提案の範囲
この提案は for-in ループの where 節のみを対象としており、他の where の用途には影響しません。
- ジェネリクスの制約としての
where(where T: Equatableなど)は、曖昧さがなく明確な利点があるため、そのまま残されます。 switchのcaseやcatch節に付くwhere節は、for-inのwhereよりも実際に使われる頻度が高く、またcaseやcatchではパターンマッチとの組み合わせで表現力を発揮しているため、今回の提案の範囲外とされました。
この提案の結末
この提案は Swift の Core Team によって Rejected(却下) となりました。for-in の where 節は Swift 3 以降もそのまま利用可能であり、現在も for x in theArray where x % 2 == 1 { ... } という記述ができます。却下の理由やその後の議論については Rationale(フォーラム投稿) を参照してください。
したがって、現時点では for-in の where 節を積極的に書き換える必要はありません。一方で、この提案で整理された「where はフィルタという単一のスタイルを優遇する」「guard の方がより柔軟で読みやすい」という観点は、コードを書く際のスタイルの指針として今でも有用です。複雑な条件や複数の早期脱出を組み合わせたいケースでは、where を避けてループ本体で guard を使うという選択肢を検討するとよいでしょう。