なんか考えてることとか

変な人が主にプログラミング関連で考えていることをまとめる。

Rustのトレイトはトレイトではない

  • 2022/02/12
    • 以下の記事にて書き直しました。「Rustのトレイトはトレイトではない」項をご覧ください。

opaupafz2.hatenablog.com

プログラミング言語「Rust」に登場する「トレイト(Traits)」は、実はSchärli氏らが提唱しているトレイトではないという話。

これについて自分は幾度となくTwitterで発言しているのだが、あまり理解・納得されるようなツイートではないと感じたので、ここでそのことについて詳細に解説することとする。

まずトレイトって何よ?

トレイトは、Nathanael Schärli氏らによって提唱された、メソッド群の集合体である。
クラスと違うのは、クラスへの実装を前提としていることであり、そういう意味では、JavaC#などのインターフェースRubyなどのMixinとよく似ている。
多重継承における問題(菱形継承問題など)を解消する目的で用意されていることが多い。

ただしインターフェースやMixinと違うのは「2つのメソッドで名前衝突した際の挙動」である。

トレイトにおける名前衝突

まず第一前提として、トレイトでは多重実装する際にトレイト同士のメソッドで名前衝突が発生した場合コンパイルする時点でエラーが発生するようになっている

以下にPHPのトレイト*1を使用したコードとその実行結果を示す。

<?php
// トレイトA
trait TraitA {
    public function method() {
        echo 'これはトレイトAのメソッドです';
    }
}

// トレイトB
trait TraitB {
    public function method() {
        echo 'これはトレイトBのメソッドです';
    }
}

// トレイトA, トレイトBをクラスに実装
class ImplClass {
    use TraitA, TraitB; // use トレイトA, トレイトB;
}
?>
PHP Fatal error:  Trait method method has not been applied, because there are collisions with other trait methods on ImplClass in ...

PHP Fatal errorPHPの重大なエラーを示す。そしてそのエラー内容にはこう書かれている。

Trait method method has not been applied, because there are collisions with other trait methods on ImplClass in ...


(ImplClassのほかのトレイトのメソッドとの衝突があるため、トレイトのmethodメソッドは適用されていません。)

以上から、トレイトでは多重実装における名前衝突が起こらないようにサポートしてくれることがわかる。

でもこれだけでは「このトレイトのメソッドだけを使いたいんだよ!」「2つとも使いたいんだよ!」という不満を持つものも現れるだろう。そこでトレイトでは、名前衝突が起こったメソッドに対し、以下の操作ができる。

  • メソッドの排除
  • メソッドの名前の変更

PHPでは、メソッドはinsteadofを使って排除できる。たとえばTraitAmethodメソッドを排除したい場合TraitBの実装時に以下のような記述を行う。

<?php

/* トレイトA, Bは省略 */

// トレイトA, トレイトBをクラスに実装
class ImplClass {
    use TraitA, TraitB {    // use トレイトA, トレイトB {
        // TraitAのメソッドを排除する
        TraitB::method insteadof TraitA;
    }
}
?>

メソッドの名前の変更はPHPではasを使う。

<?php

/* トレイトA, Bは省略 */

// トレイトA, トレイトBをクラスに実装
class ImplClass {
    use TraitA, TraitB {    // use トレイトA, トレイトB {
        // TraitAのmethodの名前をmethod_aに変更
        TraitA::method as method_a;
        // TraitBのmethodの名前をmethod_bに変更
        TraitB::method as method_b;
    }
}
?>

また、両方とも排除し、同名のまったく新しいメソッドを実装したい場合は、クラスでメソッドを定義する。

<?php

/* トレイトA, Bは省略 */

// トレイトA, トレイトBをクラスに実装
class ImplClass {
    use TraitA, TraitB; // use トレイトA, トレイトB;
    
    // 同名のまったく新しいメソッドを定義
    public function method() {
        echo 'これはクラスのメソッドです';
    }
}
?>

Rustのトレイト

さて、ここでようやく本題である。
Rustのトレイトはどのようになっているのか?それを今から解説する。

Rustではクラスがなく、代わりにプリミティブ型、構造体(struct)、列挙体(enum)、共用体(union)に実装ができる。

まず名前衝突をさせてみる。

// トレイトA
pub trait TraitA {
    fn method(&self) {
        println!("This is method in TraitA.");
    }
}

// トレイトB
pub trait TraitB {
    fn method(&self) {
        println!("This is method in TraitB.");
    }
}

// 構造体の定義
pub struct ImplStruct;

// トレイトA, トレイトBを構造体に実装
impl TraitA for ImplStruct {}   // トレイトA
impl TraitB for ImplStruct {}   // トレイトB

fn main() {
}

Rust Playground

実行してみると、奇妙なことがわかる。
なんと、コンパイルエラーにもパニック(他言語で言う例外)にもならないのである。この時点でトレイトとは何か違うが、それだけだと納得できないと思うので、インスタンスを作ってメソッドmethodを呼び出してみる。

/* トレイト, 構造体部分は省略 */

fn main() {
    // ImplStructのインスタンスを生成
    let instance = ImplStruct;
    
    // methodを実行
    instance.method()
}

Rust Playground

これを実行すると、コンパイルエラーになる。

   |
xx |     instance.method()
   |              ^^^^^^ multiple `method` found
   |

何やら「multiple `method` found(複数の`method`が見つかりました)」と言うことでエラーになった模様。

ほら、やっぱりRustのトレイトはトレイトじゃないか!

そう思った方ももしかしたらいるかもしれない。しかし、これだけでそう思うのは早計である。
Rustではメソッドチェーンによる呼び出し*2以外にも、メソッドを呼び出す方法があることをご存じだろうか。そう、メソッドを関連関数として呼び出すのである。
では、TraitAmethodと、TraitBmethodを、関連関数として呼び出してみるとどうなるだろうか。

/* トレイト, 構造体部分は省略 */

fn main() {
    // ImplStructのインスタンスを生成
    let instance = ImplStruct;
    
    // methodを実行
    TraitA::method(&instance);  // トレイトA
    TraitB::method(&instance);  // トレイトB
}

Rust Playground

This is method in TraitA.
This is method in TraitB.

実行してみると、TraitAmethodも、TraitBmethodも呼び出すことができた。
つまりここから、Rustでトレイトを多重実装すると、同名のメソッドは別個のものとして実装されることがわかる。ここがRustのトレイトと、Schärli氏らの提唱したトレイトの大きな違いである。

さらに、Rustのトレイトにはメソッドの排除もしくは名前の変更をする機能はない。強いて挙げるとするならば、構造体に実装するメソッド内部でメソッドを呼び出すことで名前を変更するのと似たようなことができるぐらいか。

/* トレイト, 構造体部分は省略 */

// 構造体にメソッドを実装する
impl ImplStruct {
    // method_a内でTraitAのmethodを呼び出す
    pub fn method_a(&self) {
        TraitA::method(self);
    }
    
    // method_b内でTraitBのmethodを呼び出す
    pub fn method_b(&self) {
        TraitB::method(self);
    }
}

fn main() {
    // ImplStructのインスタンスを生成
    let instance = ImplStruct;
    
    // methodを実行
    instance.method_a();    // トレイトA
    instance.method_b();    // トレイトB
}

Rust Playground

また、Schärli氏らの提唱したトレイトと同じく構造体に同名のまったく新しいメソッドを実装することで、コンパイルエラーはなくなる。

/* トレイト, 構造体部分は省略 */

// 構造体にメソッドを実装する
impl ImplStruct {
    // 同名のまったく新しいメソッドを定義
    pub fn method(&self) {
        println!("This is method in ImplStruct.");
    }
}

fn main() {
    // ImplStructのインスタンスを生成
    let instance = ImplStruct;
    
    // methodを実行
    instance.method();
}

Rust Playground

ただし、メソッドを排除したのではないことに注意が必要である。
なぜなら、TraitAmethodTraitBmethodはどちらも関連関数として呼び出せば呼び出すことが可能だからである。

あとがき

PHPオーバーロードのように、他言語にもある名前だけど機能としては異なるものはRustのトレイトのほかにもある。

だからRustのトレイトがトレイトではないことに対して責めるつもりは毛頭ない。

しかし、Rustのトレイトと、Schärli氏らの提唱したトレイトは異なるものであることは意識しておく必要があると考えている。

ちなみにRustにはトレイト以外にも、参照(≠C++の参照)、クロージャ(≠関数閉包)といった名前は同じだけど違うものがある。こちらももちろん別物であると意識しておく必要があると思われる。

*1:PHPのトレイトは忠実に再現したトレイトなので、安心してくれて良い

*2:「a.b()」のような呼び出し方法のこと