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 は構文的に実装を生成するだけなので、推論されたトレイト境界が間違っている場合がある。これを正しく動作させるにはいくつかの困難があると予想される。