Rustのderiveはあまり頭がよくない
概要: Rustの derive
はあまり頭がよくない。
derive
がドジを踏む例
derive
の問題は顕在化しやすく、RustコンパイラのGitHub上でも何度も重複するissueが投げられていた。今は主に #26925 を中心に議論がまとまっているので、そちらを参照するとよいだろう。
不必要な境界を与える例
use std::rc::Rc; // 本来不必要な X: Clone を要求する #[derive(Clone)] struct A<X>(Rc<X>); struct B; fn main() { A(Rc::new(B)).clone(); // Error }
error: no method named `clone` found for type `A<B>` in the current scope --> <anon>:10:19 | 10 | A(Rc::new(B)).clone(); // Error | ^^^^^ | = note: the method `clone` exists but the following trait bounds were not satisfied: `B : std::clone::Clone` = help: items from traits can only be used if the trait is implemented and in scope; the following trait defines an item `clone`, perhaps you need to implement it: = help: candidate #1: `std::clone::Clone` error: aborting due to previous error
必要な境界が不足している例
struct A<X>(X); impl<X: Copy> Clone for A<X> { fn clone(&self) -> Self { A(self.0) } } // 本来必要な X: Copy を要求しない // Error #[derive(Clone)] struct B<X>(A<X>); fn main() {}
rustc 1.17.0 (56124baa9 2017-04-24) error[E0277]: the trait bound `X: std::marker::Copy` is not satisfied --> <anon>:10:13 | 10 | struct B<X>(A<X>); | ^^^^^ the trait `std::marker::Copy` is not implemented for `X` | = help: consider adding a `where X: std::marker::Copy` bound = note: required because of the requirements on the impl of `std::clone::Clone` for `A<X>` = note: required by `std::clone::Clone::clone` error: aborting due to previous error
展開結果を見るにはnightlyコンパイラで rustc --pretty expanded src/main.rs
とやるとよい。
#![feature(prelude_import)] #![no_std] #[prelude_import] use std::prelude::v1::*; #[macro_use] extern crate std as std; use std::rc::Rc; // 本来不必要な X: Clone を要求する struct A<X>(Rc<X>); #[automatically_derived] #[allow(unused_qualifications)] impl <X: ::std::clone::Clone> ::std::clone::Clone for A<X> { #[inline] fn clone(&self) -> A<X> { match *self { A(ref __self_0_0) => A(::std::clone::Clone::clone(&(*__self_0_0))), } } } struct B; fn main() { A(Rc::new(B)).clone(); // Error }
#![feature(prelude_import)] #![no_std] #[prelude_import] use std::prelude::v1::*; #[macro_use] extern crate std as std; struct A<X>(X); impl <X: Copy> Clone for A<X> { fn clone(&self) -> Self { A(self.0) } } // 本来必要な X: Copy を要求しない // Error struct B<X>(A<X>); #[automatically_derived] #[allow(unused_qualifications)] impl <X: ::std::clone::Clone> ::std::clone::Clone for B<X> { #[inline] fn clone(&self) -> B<X> { match *self { B(ref __self_0_0) => B(::std::clone::Clone::clone(&(*__self_0_0))), } } } fn main() { }
deriveはマクロの仲間
derive
はマクロと同様の構文拡張に分類される。Rustコンパイラは原則として「構文解析→構文拡張の展開→名前解決」の順で進む(ただし、パスマクロの解決のために名前解決が先に実行されることはある)ため、 derive
実行時は名前解決がなされておらず、型の情報もほぼない状態である。
deriveの境界生成規則
derive
は境界を決めるために、以下のようにして型を収集する。
- 全ての型引数。
- フィールドの型の一部分として登場する型のうち、型引数名で始まるパス型。ASTでの修飾パスのデータ構造の性質から、以下のいずれかの条件を満たせばマッチする。
X::Output::Item
のように、型引数名で始まるパス。<Frob>::X::Output
のようなパス (QSelfが削除されてX::Output
になってしまう) (※おそらくバグ)<Frob as X::Output>::Bar::Baz
のようなパス (QSelfが削除されてX::Output::Bar::Baz
になってしまう) (※おそらくバグ)
おそらくバグだが、 <X>::Output
のように書くとマッチしない。
例えば derive(Clone)
の場合は、このようにして集めた各型に対して、 Clone
を実装すべしという制約を課す。他の多くの derive
も同じ機構を用いている。
なぜフィールド型に直接境界を課さないのか
上記のようなヒューリスティクスを用いず、フィールド型に直接 Clone
を課せるようにするのが好ましいが、以下のような問題がある。
問題1: 可視性
現在のRustでは “private in public” 制約というものがある。詳しくは過去の記事を参照。 これにより以下のようなコードがエラーになる。
#[derive(Clone)] struct A<X>(X); pub struct B<X>(A<X>); // Error impl<X> Clone for B<X> where A<X>: Clone { fn clone(&self) -> Self { B(self.0.clone()) } } fn main() { let x = B(A(0u32)); x.clone(); }
B<X>
は公開される型だがそのメンバ型 A<X>
は非公開である。そのため X: Clone
は課すことができても A<X>: Clone
を課すことはできない。
問題2: 再帰的な制約
以下のように再帰的な型に対して derive(Clone)
をすることを考える。現在のヒューリスティックスでは正しく動作するが、以下のようにフィールド型に直接 Clone
を課すと、実体化のタイミングで再帰エラーになる。なお、実体化させない限りは問題が顕在化しない。
struct Node<T>(Option<Box<(Node<T>, T)>>); impl<T> Clone for Node<T> where Option<Box<(Node<T>, T)>>: Clone { fn clone(&self) -> Self { Node(self.0.clone()) } } fn main() { let x = Node(Some(Box::new((Node(None), 0u32)))); x.clone(); // Error }
問題3: 後方互換性
トレイトは実装を増やしても減らしても互換性がない。そのため、今から derive(Clone)
の挙動を変えると大きな破壊的変更となる可能性がある。
まとめ
Rustの derive
は構文的に実装を生成するだけなので、推論されたトレイト境界が間違っている場合がある。これを正しく動作させるにはいくつかの困難があると予想される。