!!“Unwrap or Die”演算子をSwift標準ライブラリに導入する
Introducing the !! “Unwrap or Die” operator to the Swift Standard Library
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
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 の問題意識です。precondition や assert がメッセージを受け取れるのと同じように、アンラップでも「なぜ安全だと言えるのか」を一行で書き添えたい、というわけです。
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 つありました。
- アンラップ失敗の理由を実行時まで運ぶ。
precondition("...")やassert("...")のように、トラップ時のコンソール出力に根拠がそのまま残るので、デバッグ時に「なぜここがnilになってはいけないはずだったのか」をソースまで遡らずに知ることができます。 - 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("理由") }を書くか、プロジェクトやチームで!!相当の演算子を自前で定義する、という選択になります。 - fixit については、
Insert "!"一択という状況を改めるかどうかは引き続きツール側の課題として残りました。
ダイジェストとして押さえておきたいのは、「アンラップに根拠を添えたい」という需要自体は認められたものの、その解決策として専用演算子を増やすよりも、Never の扱いや既存のツール側の改善で対応するほうが筋が良い、という方向に Swift が舵を切った、という点です。
03 今後の見通し
この proposal は Rejected となったため、ここで述べられている「今後の見通し」は実現された Swift の機能ではなく、提案者が !! の延長線上で見ていた将来像 です。いずれも構想として記録されているもので、実現を約束するものではありません。
呼び出し位置のファイル名・行番号を出す
提案された !! の実装には、ひとつ実用上の弱点がありました。失敗時に fatalError が報告するファイル名と行番号が、!! を呼び出した利用側ではなく、!! 演算子の実装が書かれた標準ライブラリ側になってしまう、という点です。
これは Swift の中置演算子が引数を 2 つしか取れない、という当時の制約に由来します。もし将来 Swift が 3 引数以上の演算子を許すようになれば、!! も次のように #file / #line をデフォルト引数で受け取れるようになり、利用側のソース位置を報告できるようになる、というのが提案者の見通しでした。
public static func !!(
optional: Optional,
errorMessage: @autoclosure () -> String,
file: StaticString = #file,
line: UInt = #line
) -> Wrapped
Never を真の bottom type として扱う
もうひとつの方向性は、SE-0102 で言及されていた「Never を真の bottom type として扱う」案の実現です。Never がすべての型のサブタイプとして扱えるようになれば、fatalError の戻り値(Never)を任意の型として使えるので、専用演算子を導入しなくても次のように既存の ?? で !! 相当を表現できます。
let x = y ?? fatalError("理由")
提案者自身もこの方向性が望ましいことは認めており、!! を導入することと Never を bottom type として整備することは矛盾しない、と述べています。実際に Core Team が Rejected の理由として挙げたのも、まさにこの「Never の整備で同等のことができるなら専用演算子は不要」という判断でした。!! は採用されませんでしたが、この Never の bottom type 化自体は依然として将来の検討対象として残されています。