「なぜRustでは外部クレートのトレイトを外部型に実装できないのか?」——Rustを書き始めてしばらくすると必ず壁にぶつかるこの制約、実はエコシステム全体の健全性に深く関わっています。今回紹介する記事「An Incoherent Rust」は、Rustのコヒーレンス(coherence)とオーファンルール(orphan rules)が引き起こす構造的問題を鋭く指摘したものです。
背景:serdeの例で見るエコシステムの歪み
Rustエコシステムの中核を担う serde は Serialize / Deserialize というトレイトを定義しています。問題は、あるクレートが serde に対応するためにこれらのトレイトを自分の型に実装すると、今度は別のシリアライゼーションライブラリ(仮に nextserde とする)が登場した際に全クレートが個別対応しなければならないという点です。
serde対応済みクレートが100個あれば、nextserdeへの移行には100個すべての修正が必要- 対応されなければユーザーは全クレートをフォークしてパッチを当てるしかない
- 結果として「最初に普及したクレートが質に関わらず居座り続ける」インセンティブが生まれる
これは開発者の怠慢ではなく、言語仕様そのものが引き起こす構造問題です。
技術的仕組み:コヒーレンスとオーファンルール
コヒーレンスとは「あるトレイトはある型に対して最大1回しか実装できない」というルールです。
trait Trait {}
trait Thingies {}
trait OtherThingies {}
impl<T: Thingies> Trait for T {}
impl<T: OtherThingies> Trait for T {} // error[E0119]: conflicting implementations
オーファンルールはこのコヒーレンスを保証するための制約で、「トレイト実装は、トレイトか Self 型のどちらかが現在のクレートで定義されている場合のみ書ける」というものです。
// crate a
pub trait Trait {}
pub struct Foo;
// crate b
use a::*;
impl Trait for Foo {} // error[E0117]: only traits defined in the current crate can be implemented
なぜコヒーレンスは必要なのか:HashMapの悪夢
記事が挙げる最もわかりやすい例が HashMapの問題です。
// crate b: MyDataのHashを独自定義してHashSetを構築
fn make_hashset() -> HashSet<MyData> { ... }
// crate c: 別のHash実装(常に0を返す最悪実装)
fn check_hashset(set: HashSet<MyData>) {
assert!(set.contains(MyData(1))); // 永遠に失敗する
}
// crate d
c::check_hashset(b::make_hashset()); // 全く意味をなさない結果に
コヒーレンスがなければ、同一型に対して異なるクレートが矛盾するトレイト実装を持てるようになり、実行時に完全に壊れた動作を引き起こします。さらに深刻なのは健全性(soundness)の問題で、型システムレベルで未定義動作を引き起こす可能性があることも記事は指摘しています。
まとめ・所感
コヒーレンスは「正しい」制約です——問題なのはその副作用としてエコシステムの競争が阻害される点にあります。Rustコア開発者のNiko Matsakisも同様の問題意識を持っており、クレートレベルのwhere句などの解決策が議論されています。
Rustでライブラリを設計する立場にある方、あるいは「なぜnewtype patternが推奨されるのか」を深く理解したい方にとって、この記事はトレイト設計の本質を見直す良い機会になるでしょう。元記事ではサウンドネス問題のコード例も詳しく掲載されているので、ぜひ合わせて読んでみてください。