Swift Digest
SE-0276 | Swift Evolution

Multi-Pattern Catch Clauses

Proposal
SE-0276
Authors
Owen Voorhees
Review Manager
Doug Gregor
Status
Implemented (Swift 5.3)

01 何が問題だったのか

Swift の do-catch 文では、従来ひとつの catch 節に書けるパターンはひとつだけで、where 節も 1 組しか付けられませんでした。これは switch 文の case が「カンマ区切りで複数のパターンをまとめ、共通の本体を実行する」書き方をサポートしているのと対照的で、パターンマッチングの仕様に一貫性がありませんでした。

この制約のもとでは、複数のエラーケースに対して同じ処理を行いたいときに、次のような書き方ができません。

do {
  try performTask()
} catch TaskError.someRecoverableError {    // OK
  recover()
} catch TaskError.someFailure(let msg),
        TaskError.anotherFailure(let msg) { // 現状は不可
  showMessage(msg)
}

そのため実際には、次のどちらかで回避する必要がありました。

  • catch 節を複数に分け、本体の処理を重複して書く。
  • catch 節の中で switch をネストさせ、そこでパターンをまとめる。
do {
  try performTask()
} catch let error as TaskError {
  switch error {
  case TaskError.someRecoverableError:
    recover()
  case TaskError.someFailure(let msg),
       TaskError.anotherFailure(let msg):
    showMessage(msg)
  }
}

前者は本体の重複を生み、後者は catch 節のパターンマッチング機能をわざわざ無効化して内側で switch をやり直す形になるため、catch でパターンマッチングをサポートしている意義を損なってしまいます。複数のエラーケースを同じ本体で処理したい、という素直なニーズに対して、コードが冗長になりがちだったのが問題でした。

02 どのように解決されるのか

catch 節の文法を拡張し、switchcase と同じように、ひとつの catch にカンマ区切りで複数のパターン(それぞれに任意の where 節を付けられる)を並べられるようにします。実行時に投げられたエラーが、そのうちいずれかのパターンにマッチし、かつ先行する catch 節にマッチしていなければ、その catch の本体が実行されます。

これにより、冒頭の例をそのまま書けるようになります。

do {
  try performTask()
} catch TaskError.someRecoverableError {
  recover()
} catch TaskError.someFailure(let msg),
        TaskError.anotherFailure(let msg) { // こう書ける
  showMessage(msg)
}

performTaskTaskError.someFailure("message") または TaskError.anotherFailure("message") を投げた場合、2 つ目の catch の本体が実行され、showMessage(msg) が呼ばれます。

値の束縛

switchcase と同様に、複数パターンを持つ catch 節でも値の束縛を行えます。ただしその場合、switch と同じく、同じ名前の束縛を同じ型ですべてのパターンに含める必要があります。上の例の msg がまさにそのパターンで、両方のパターンで同名・同型で束縛されているため、本体でそのまま利用できます。

文法

拡張後の catch 節の文法は次のようになります。

catch-clauses -> catch-clause catch-clauses?

catch-clause -> 'catch' catch-item-list? code-block

catch-item-list -> catch-item |
                   catch-item ',' catch-item-list

catch-item -> pattern where-clause? |
              where-clause

trailing closure との関係

パース上の曖昧さを避けるため、catch 節のアイテムの中では、末尾に trailing closure を持つ式は書けません。これは switchcase とは異なる挙動ですが、実用上は、必要なら明示的にカッコで囲んで書けば対応できます。

今後の展望

Proposal には今回のスコープ外として、将来の検討対象がいくつか speculative に挙げられていました。いずれも実現が約束されているわけではありません。

  • すべての catch 節で暗黙のエラー束縛を使えるようにする案。現在はパターンを持たない catch 節でのみ暗黙の error: Error が利用できますが、これを全 catch 節に広げると再スローなどが書きやすくなります。ただし error という名前をそのまま使うとソース互換性への影響が大きいため、$error のようなコンパイラ定義の識別子を使う形が検討されています。
  • catch 節での fallthrough サポート。switchcase とさらに仕様を揃える方向ですが、外側の switch に対する fallthrough と衝突してソース互換性を壊すこと、他言語での前例がないこと、具体的な利用シーンが確認されていないことから、現時点では導入しない方針です。
  • catch 節を条件付きコンパイルブロックで囲めるようにする案。switchcase に対する既存サポートと揃える方向ですが、条件付きコンパイル全体の再設計と合わせて別途検討される想定です。