Swift Digest
SE-0217 | Swift Evolution

Introducing the !! “Unwrap or Die” operator to the Swift Standard Library

Proposal
SE-0217
Authors
Ben Cohen, Dave DeLong, Paul Cantrell, Erica Sadun
Review Manager
Joe Groff
Status
Rejected

01 何が問題だったのか

Swift の強制アンラップ演算子 ! は、Optional 値が nil のときにアプリをトラップさせてしまう危険な操作です。それでも「ここは絶対に nil にならない」とわかっている箇所では使いたい場面があります。従来は、理由をコメントで添えるのが一般的な作法でした。

let lastItem = array.last! // Array guaranteed to be non-empty because...

しかし、この書き方には大きな欠点があります。実際にトラップが起きたとき、クラッシュログに残るのは「unexpectedly found nil」のような定型メッセージだけで、コメントに書いた理由は実行時には一切出力されません。トラブル対応では、ログの行番号からソースを開き、周辺のコメントを読み、文脈を再構築する必要があります。自分が書いたコードでなければなおのこと厄介です。

もうひとつの問題は、Swift コンパイラが出す「Insert "!"」という fixit です。Optional を未アンラップのまま使おうとするとコンパイラが ! を挿入する修正案を提示しますが、これは構文上のエラーを消すだけで、意味的には正しくないことが多くあります。

let resourceData = try String(contentsOf: url, encoding: .utf8)
// Error: Value of optional type 'URL?' not unwrapped; did you mean to use '!' or '?'?
// Fix: Insert '!'

経験豊富な開発者であれば、この Optional が「たまたま Optional になってしまっているだけなのか」「本当に nil になり得るのか」を判断し、宣言側を直すか if let / guard let で受けるかを選べます。一方、初学者は fixit のボタンを押して ! を挿入し、そのままコンパイルを通してしまいがちです。結果として、Stack Overflow やフォーラムは「unexpectedly found nil」で埋め尽くされることになります。

つまり Swift には、

  • 「なぜ nil にならないと言えるのか」という根拠を実行時のエラーメッセージにまで運ぶ仕組み
  • 初学者を安易な ! 挿入から遠ざけ、guard let / if let や根拠付きアンラップへと導く fixit

の両方が欠けていた、というのがこの proposal の問題意識です。preconditionassert がメッセージを受け取れるのと同じように、アンラップでも「なぜ安全だと言えるのか」を一行で書き添えたい、というわけです。

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

この proposal は Rejected となり、Swift 標準ライブラリには !! 演算子は導入されませんでした。 ただし提案された設計そのものは、コミュニティで広く使われている「unwrap or die」パターンを言語機能として取り込むものであり、検討の流れを理解しておくと、なぜ Swift が今の形に落ち着いているのかがわかります。

提案された !! 演算子

新たに中置演算子 !! を標準ライブラリに追加し、?? と同じ NilCoalescingPrecedence に置きます。左辺が Optional、右辺が文字列(の autoclosure)で、左辺が nil のときは右辺のメッセージとともに fatalError でトラップする、というものです。

let lastItem = array.last !! "Array guaranteed to be non-empty because..."

let url = URL(string: "http://swift.org")
       !! "URL is not well formed"

これは次の guard と意味的に等価な糖衣として位置付けられていました。

guard let value = wrappedValue else {
    fatalError("Explanation why lhs cannot be nil.")
}

実装としてもシンプルで、Optional に対する静的メソッドとして次のように定義する想定でした。

infix operator !!: NilCoalescingPrecedence

extension Optional {
  public static func !!(
    optional: Optional,
    errorMessage: @autoclosure () -> String
  ) -> Wrapped {
    guard let wrapped = optional else { fatalError(errorMessage()) }
    return wrapped
  }
}

右辺が autoclosure なので、左辺が nil でない限りメッセージ文字列の構築コストは発生しません。

ねらい

!! の狙いは 2 つありました。

  1. アンラップ失敗の理由を実行時まで運ぶprecondition("...")assert("...") のように、トラップ時のコンソール出力に根拠がそのまま残るので、デバッグ時に「なぜここが nil になってはいけないはずだったのか」をソースまで遡らずに知ることができます。
  2. fixit の改善。Optional を未アンラップで使ったときの fixit を Insert "!" から Insert '!!' <# "Explanation why lhs cannot be nil." #> に差し替え、初学者に説明文を書く機会を与える、という副次効果を狙っていました。

また、!!! は排他ではなく、! は引き続き残す前提でした。説明が不要なら従来どおり ! を、「なぜ安全か」を記録に残したいなら !! を選ぶ、という使い分けです。

なぜ Rejected になったのか

Core Team のレビュー結果(Decision Notes のフォーラム投稿)では、主に次のような点が否定的に評価されました。

  • 新しい演算子を追加するコストに見合うだけの価値があるか疑わしい!! で得られる表現力は、コミュニティの一部がユーティリティとして定義すれば十分まかなえる範囲であり、標準ライブラリに刻む必然性は弱いと判断されました。
  • ! を撲滅したいわけではない。Swift は「! は控えめに、でも必要なときは使う」という立場であり、!! を標準化することで ! の使用を暗に非推奨とするような誤ったメッセージを送りかねない、という懸念がありました。
  • より望ましい方向は別にあるNever を真の bottom type として整備すれば、array.last ?? fatalError("...") のように既存の ??fatalError を組み合わせるだけで同じ目的を達成できます。専用演算子を増やすより、こちらの言語基盤の整備を優先すべきだ、という見方です。

利用者としての現在地

結果として Swift 本体には !! は入らず、利用者から見た状況は以下のようになっています。

  • 根拠付きの強制アンラップが欲しい場合は、従来どおり guard let ... else { fatalError("理由") } を書くか、プロジェクトやチームで !! 相当の演算子を自前で定義する、という選択になります。
  • 将来 Never が bottom type として扱えるようになれば、let x = y ?? fatalError("理由") のような書き方が !! の代替として自然に使えるようになる可能性があります(これはあくまで見通しであり、実現が約束されたものではありません)。
  • fixit については、Insert "!" 一択という状況を改めるかどうかは引き続きツール側の課題として残りました。

ダイジェストとして押さえておきたいのは、「アンラップに根拠を添えたい」という需要自体は認められたものの、その解決策として専用演算子を増やすよりも、Never の扱いや既存のツール側の改善で対応するほうが筋が良い、という方向に Swift が舵を切った、という点です。