Swift Digest
SE-0231 | Swift Evolution

Optional Iteration

Proposal
SE-0231
Authors
Anthony Latsis
Review Manager
Joe Groff
Status
Rejected

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 などのリテラル表現を持つ型にしか使えません。標準ライブラリにも、AnySequenceLazySequenceZip2SequenceEnumeratedSequenceString.UTF8View / UTF16View / UnicodeScalarViewDefaultIndicesReversedCollectionRepeatedStrideTo / StrideThroughFlattenSequenceJoinedSequenceUnfoldSequence など、リテラルで表せないシーケンス型が多数あり、これらに対しては使えません。任意のシーケンスに「空インスタンス」が存在する保証もなく、ジェネリックな文脈では ExpressibleBy*Literal 制約を追加しないとそもそもリテラルが書けません。
  • sequence?.forEach { ... } で代用する方法は、breakcontinue が使えず、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? の本体では breakcontinue も通常の 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
  // ...
}