PaPoo
cover
technews
Author
technews
世界の技術ニュースをリアルタイムでキャッチし、日本語でわかりやすく発信。AI・半導体・スタートアップから規制動向まで、グローバルテックシーンの「今」をお届けします。

Rust の ! を10年止めていたのは、型ではなく“()への落下”だった

この記事は後半です。前半は Qiita にあります: 値を持たない型がなぜ10年も止まったのか、Rust ! の“落下先”を追う

! の扱いは、表面だけ見ると単純です。return や panic! のように値を返さない式は ! になり、その ! は他の型へ強制変換できます。問題は、その変換先がはっきり決まらないときに起きます。Rust の説明では、コンパイラは ! を見つけると内部的に absurd(panic!()) のような形に置き換え、absurd<T>(x: !) -> T の T を推論しようとします。ここで行き先が決まらないと、歴史的には fallback として () が入っていました。

画像: github.com より
画像: github.com より

この fallback が厄介なのは、単に「型が決まらないから仮で置く」以上の意味を持っていたからです。! は値を持てないので、本来は「ここには到達しない」という事実のまま扱いたい。ところが () に落ちると、到達不能な式が「単位値を返す式」として扱われます。見かけ上は通っても、型システムの上では別物です。Rust 2024 で fallback が ! に変わったのは、この不自然さを解消するためでした。

直ったのは型推論で、消えたのは違和感

この変更で一番大きいのは、! が理由なく () として扱われなくなったことです。以前は、型が最後まで閉じない場所で ! が unit に吸われ、後から見返すと「なぜここが () なのか」が分からない場面がありました。? 演算子や、panic!() を含むクロージャ、if の片側が return になる分岐などで、推論の都合がそのまま見た目に出ていたわけです。

Edition Guide にある例は、そのまま現場感があります。f()?; のように、? の戻り先がジェネリックだと、以前は () に寄ることで通っていたコードが、今は ! のまま残って推論できなくなることがあります。また、panic!() を返すクロージャも、以前は () を期待するトレイト制約に乗って通っていたものが、今は ! がそのまま残るので、クロージャに || -> () と戻り値の型を明示しないといけない場合があります。

ここで重要なのは、Rust が厳しくなったというより、曖昧さを隠さなくなったことです。! が () に化けると、一見便利でも、実は型の不足を見えなくしていました。! のまま残るようになってからは、その場で必要な型をはっきり書かないといけない。手間は増えますが、推論の帳尻合わせで意味が変わるよりずっと筋が通っています。

互換性を壊さずに直す難しさ

では、なぜこれが 10 年も止まっていたのか。理由は単純で、正しい挙動に変えると古いコードが壊れるからです。! の fallback を () にしていた時代には、その挙動に依存したコードやクレートがすでに動いていました。Rust Project Goals でも、! の安定化に向けた作業として、never type fallback への依存を減らすことが挙げられています。つまり、問題は理論ではなく移行でした。

Issue #66757 のような報告を見ると、その難しさが分かります。TryFrom と From を組み合わせた ? の経路で、期待したエラー型ではなく () が見えてしまう。しかも、Infallible や ! の変換が絡むと、どこで fallback が効いたのかを追うのが面倒です。単一の修正で片づく話ではなく、コンパイラ側の既定値、標準ライブラリの型名、既存クレートの実装が全部つながっていました。

Rust 1.100 で Infallible が ! のエイリアスになったのも、この整理の一部です。昔からの API を切らずに、! を正式な型として扱うための橋渡しです。ただし、公式リリースノートも、fallback の変更は一部の稀なケースで挙動を変えると明記しています。安定化はしたが、完全に無風ではない。そこが現実です。

自分なら、どこでこの変更を意識するか

実際に気をつける場面は、派手なアプリ本体より、型が濃いライブラリ層です。? を多用するコード、ジェネリクスの戻り値が未確定な関数、早期 return を含む補助関数、こういう場所は今でも ! の影響を受けやすいです。もし昔の Rust で「なぜか () が混ざる」と感じていたなら、まずそこを疑うのが自然です。

逆に言えば、! が安定した今は、無理に () を前提にした逃げ道を書かなくてよくなりました。到達不能な分岐は ! のまま置き、必要なところだけ型注釈を足す。panic!() を雑に流し込むより、どこで値が生まれ、どこで生まれないのかをコード上で明示したほうが、後から読む人にも親切です。Rust が ! を安定化した意味は、記号を増やしたことより、曖昧な unit への落下をやめたことにあります。

同じ著者の記事