Optional Iteration
01 何が問題だったのか
Swift の多くの文には、Optional に対するショートカット的な構文が用意されています。if let / guard let による optional binding、switch の直接的な Optional マッチ、optional chaining (foo?.bar)、optional 呼び出し (foo?())、optional assignment (dict[key]? = value) など、「値があればそれを使い、nil なら何もしない」という流儀が言語全体に根付いています。
ところが for-in ループだけは Optional のシーケンスを直接扱えません。optional chaining や dictionary の subscript、JSON のデコード結果など、プログラムの中で Optional<Sequence> が生じる場面は意外と多いのですが、それらを素直にループに渡すことはできず、一度アンラップしてからでないと回せません。
if let sequence = optionalSequence {
for element in sequence { ... }
}
ワークアラウンドはいくつかありますが、どれも一般解にはなりません。
guardで早期 return するスタイルはシンプルなケースでは有効ですが、nilのときに抜ける必要がない(後続の処理がnilでも成り立つ)状況では、無理にguardに寄せると制御フローが歪み、読みづらくなります。?? []のような空リテラルでのフォールバックは、ExpressibleByArrayLiteralなどのリテラル表現を持つ型にしか使えません。標準ライブラリにも、AnySequence、LazySequence、Zip2Sequence、EnumeratedSequence、String.UTF8View/UTF16View/UnicodeScalarView、DefaultIndices、ReversedCollection、Repeated、StrideTo/StrideThrough、FlattenSequence、JoinedSequence、UnfoldSequenceなど、リテラルで表せないシーケンス型が多数あり、これらに対しては使えません。任意のシーケンスに「空インスタンス」が存在する保証もなく、ジェネリックな文脈ではExpressibleBy*Literal制約を追加しないとそもそもリテラルが書けません。sequence?.forEach { ... }で代用する方法は、breakやcontinueが使えず、returnがクロージャ内のreturnにしかならないなど、for-inとは意味論が異なります。制御フロー文を含むループの代替にはなりません。
つまり、Optional を自然に扱うための構文が言語の他の部分には揃っているのに、for-in にだけ穴が空いている、というのがこの提案の出発点でした。
02 どのように解決されるのか
この提案は、Optional のシーケンスを直接回せる新しい構文 for? を導入するものでした。ただし、本提案は Rejected となり、Swift には取り込まれていません。
提案されていた構文
for? は、シーケンスが nil のときはループを丸ごとスキップし、値があるときだけ通常どおり反復するというものです。意味的には次の二つは等価です。
let array: [Int]? = nil
for? element in array { ... }
// 等価なコード
if let unwrappedArray = array {
for element in unwrappedArray { ... }
}
動作としては、sequence?.makeIterator() の時点で nil であればそこで止まり、何も実行せずに次の文へ進みます。for? の本体では break や continue も通常の for-in と同じように使えます。
? は単なる構文上のマーカーで、for! のような対になるバリアントはありません。Optional のシーケンスに対して ? を付けずに書くとエラーで、fix-it が for? を勧めてきます。逆に非 Optional のシーケンスに for? を付けてもエラーになります。
let array: [Int] = [1, 2, 3]
let optArray: [Int]? = nil
for element in optArray {
// error: must be unwrapped; fix-it suggests 'for?'
}
for? element in array {
// error: optional for-in loop must not be used on a non-optional sequence
}
これは Swift が「nil を暗黙に握りつぶす箇所には必ず ? を書かせる」という一貫したスタイル(optional chaining 等と同じ流儀)を for-in にも適用するためのものです。
なぜ ? をあえて必須にしたのか
for の右辺に Optional を置けるようにするだけでも、構文上は新しい記号を増やさずに済みます。しかし、その場合は「nil なら何もしない」という挙動が一切の印なしに起きることになり、Swift が switch や ?. で貫いているスタイル(silent な nil ハンドリングには ? を伴わせる)から外れます。提案では clarity を優先し、for? という明示マーカーを導入する形が選ばれました。
最終的な扱い
この提案は Swift Evolution のレビューを経て Rejected となりました。判断の要旨は Swift フォーラムの Rejection rationale で公開されていますが、要点としては、for? のユースケースは ?? []、for element in sequence ?? []、sequence?.forEach、あるいは if let によるアンラップなど既存の手段で十分賄え、言語に新しい構文を追加するほどの効果が見込めない、というものでした。
したがって、現在の Swift で Optional のシーケンスを回したい場合は、引き続き次のいずれかを使うことになります。
let optionalArray: [Int]? = nil
// 1. 空でフォールバック(配列など ExpressibleBy*Literal の型に限る)
for element in optionalArray ?? [] {
// ...
}
// 2. optional binding でアンラップしてからループ
if let array = optionalArray {
for element in array {
// ...
}
}
// 3. forEach(break / continue は使えない)
optionalArray?.forEach { element in
// ...
}