2019/02/04

Rustでのtraitの便利な使い方

classがないRust

Rustではclassがありません。例えばPythonで以下のようなコードがあるとします。




>>> class Action:
...     def run(self):
...         return "running"
...
>>> class Human(Action):
...     pass
...    
>>> class Car(Action):
...     pass
...
>>> human = Human()
>>> car = Car()
>>> human.run()
'running'
>>> car.run()
'running'
>>>    

この場合、上記のようにActionクラスのサブクラスであるHumanクラスとCarクラスはスーパークラスであるActionクラスのメソッドrunをそれぞれ定義しなくても使うことができます。


ではクラスのないRustで同じことをするにはどうすればよいか?


Rustではstructtraitを用いることで、同じことができます。



trait Action {
    fn run(&self);
}

struct Human;

impl Action for Human {
    fn run(&self) {
        println!("running");
    }
}

struct Car;

impl Action for Car {
    fn run(&self) {
        println!("running");
    }
}

fn main() {
    let human = Human;
    let car = Car;
    
    human.run();
    car.run();
}

traitにあるメソッドと同じメソッドをそれぞれに書かなければなりません。一見面倒に見えるこの作業ですが、慣れると気になりません。そしてさらに便利に活用できるのがtraitです。例えば、以下のようにrunというメソッドを持ったCmdトレイトなるものと3つの構造体(Echo、Sum、Prod)を作成します。そして上記のコードのようにそれぞれにCmdトレイトを適用します。



trait Cmd {
    fn run(&self);
}

struct Echo(String);

impl Cmd for Echo {
    fn run(&self) {
        println!("{}", self.0);
    }
}

struct Sum(Vec<i32>);

impl Cmd for Sum {
    fn run(&self) {
        println!("{}", self.0.iter().sum::<i32>());
    }
}

struct Prod(Vec<i32>);

impl Cmd for Prod {
    fn run(&self) {
        println!("{}", self.0.iter().product::<i32>());
    }
}

fn eval<C>(cmd: &C) where C: Cmd {
    cmd.run();
}

fn main() {
    let echo = Echo("Hello, world!".to_string());
    let sum = Sum(vec![1,2,3,4,5]);
    let prod = Prod(vec![1,2,3,4,5]);
    eval(&echo);
    eval(&sum);
    eval(&prod);
}

このコードで重要な役割を担うのがジェネリック関数evalです。evalはCmdトレイトを実装する任意の型Cの参照を引数にとる関数となります。つまり、Cmdトレイトを実装していればどのようなものでも受け付けることができます。パスポートを例に挙げるとわかりやすいかもしれません。海外旅行はパスポートがなければ入国できません。同様にevalを国に例えるならば、eval国にはパスポートであるCmdトレイトを持っていなければ入国できない、といったイメージでしょうか。さらにこのような制約をより厳しくすることもRustでは容易に実装することができますし、トレイトのサブトレイトなるものを作成することもできます。evalのところは、以下のように書いても同じ結果になります。



fn eval<C: Cmd>(cmd: &C) {
    cmd.run();
}

whereを省略した書き方です。この上記2つのやり方はトレイト境界を使った静的ディスパッチというやり方で、コンパイル時にCにあてはまる型それぞれ専用の同名の関数が作成され、呼び出し部分を書き換えるそうです。コンパイル時に諸々のサイズが分かるため、インライン化・最適化ができて高速な反面、すでにある関数のコピーをそれぞれ専用に作るため、最終的なコードが膨張してしまうという欠点もあるようです。
一方で、以下のようなやり方もあります。



fn eval(cmd: &dyn Cmd) {
    cmd.run();
}

このやり方はトレイトオブジェクトを使った動的ディスパッチというやり方で、静的ディスパッチとは違い、コンパイル時に諸々のサイズがわからないため、インライン化・最適化できない反面、コードの膨張を少なくすることができるようです。


と知ったかぶった発言をしておりますが、私もまだまだ勉強の身、早くトレイトの底力を完全に身に着けたいところです。私ももっと勉強して上手に説明できるようにせねばなりません。メールでご指摘いただいたFさんありがとうございます。勉強に励みます。