nがひとつ多い。

えぬなおの技術的なことを書いていくとこ。

【Rust】コードで理解するトレイト

はじめに

主に僕は書籍を通じてRustを勉強していたが、はっきり言って全く理解できなかったので生存期間に引き続きコードで記述する事で理解しようと思った。

Rustのモジュールの考え方について

Rustは任意の型に対して通常 impl を用いて以下のようにモジュールを追加する。

struct Cat {
    name: String,
    eyes: String,
}

impl Cat {
    fn new(name: &String, eyes: &String) -> Self {
        Cat {
            name: name.to_string(),
            eyes: eyes.to_string(),
        }
    }
    fn cry(&self, cry_str: String) {
        println!("{} eyes {} say, {}", &self.eyes, &self.name, cry_str)
    }
}

fn main() {
    let cat = Cat::new(&"JiJi".to_string(), &"Black".to_string());
    cat.cry("myaw".to_string());  //-> Black eyes JiJi say, myaw
}

Rust Playground

Goをやっていた人からすると、コードの流れとしては Go にすごい似ていることに気づくだろう。 Cat 型を定義した後、impl キーワードを使って fn cry(&self, cry_str: String) メソッド関数を定義している。

こうする事で、new または cry 関数は Cat 型専用の関数となるので、保守性と可読性が増すという訳だ。

Rustのモジュールでの課題

こうする事で、new または cry 関数は Cat 型専用の関数となるので、保守性と可読性が増すという訳だ。

この通り、"new または cry 関数は Cat 型専用の関数" となってしまう訳で、例えば他にも声を出して鳴くであろう同様の DogDevilPikachu など定義する時に、コードが冗長化する。

そこで今回の トレイト (trait)が登場する訳だ。

やっていき

ようは、この new またはcryするという機能 というものAnimalという名前の trait として定義して、型に一段DIするという運びとなる。

struct Cat {
    name: String,
    eyes: String,
}

struct Dog {
    name: String,
    eyes: String,
}

trait Animal {
    fn new(name: &String, eyes: &String) -> Self;
    fn cry(&self, cry_str: String);
}

fn animal_new<T: Animal>(name: &String, eyes: &String) -> T {
    Animal::new(&name.to_string(), &eyes.to_string())
}

fn animal_cry<T: Animal>(animal: T, cry_str: String) {
    animal.cry(cry_str);
}


impl Animal for Cat {
    fn new(name: &String, eyes: &String) -> Self {
        Cat {
            name: name.to_string(),
            eyes: eyes.to_string(),
        }
    }
    fn cry(&self, cry_str: String) {
        println!("Cat, {} eyes {} say, {}", &self.eyes, &self.name, cry_str)
    }
}

impl Animal for Dog {
    fn new(name: &String, eyes: &String) -> Self {
        Dog {
            name: name.to_string(),
            eyes: eyes.to_string(),
        }
    }
    fn cry(&self, cry_str: String) {
        println!("Dog, {} eyes {} say, {}", &self.eyes, &self.name, cry_str)
    }
}

fn main() {
    let cat: Cat = animal_new(&"JiJi".to_string(), &"Black".to_string());
    let dog: Dog = animal_new(&"Mugi".to_string(), &"Red".to_string());
    animal_cry(cat, "myaw".to_string());  //-> Cat, Black eyes JiJi say, myaw
    animal_cry(dog, "bowow".to_string());  //-> Dog, Red eyes Mugi say, bowow
}

Rust Playground

解説はじめ

トレイト単体で言えば、C++でいう仮想関数や、Goでいうinterfaceと言ったものに非常に似ている。
はじめに animal_new 関数により、2つの異なる型を同時に初期化している。
中身が注目で2つの異なる型のnew メソッドを Animal トレイトから出せるようになっている事がわかる。
これにより、 Cat型と Dog型を同等に扱える。
そして以下に何やら怪しい形の関数を定義していることに注目されたい。

fn animal_new<T: Animal>(name: &String, eyes: &String) -> T {
    Animal::new(&name.to_string(), &eyes.to_string())
}

<snip>

    let cat: Cat = animal_new(&"JiJi".to_string(), &"Black".to_string());
    let dog: Dog = animal_new(&"Mugi".to_string(), &"Black".to_string());

<snip>

型もメソッドもDIしたのであれば、せっかくなので同じ関数で呼び出したいとなるので、満を辞して ジェネリクス さんの登場してもらうことになる。 Rustのジェネリクスはとても賢いので、定義したtraitを紐づけてやると、制限つきのジェネリクス、つまりAnimalトレイトをimplしている型だけ使えるようにジェネリクスを縛れる
こうして定義した関数を ジェネリクス関数 と呼ぶらしい。
縛られているか証拠に試しに Flog型 を定義してみると期待通りコンパイルエラーとなる。

struct Flog {
    name: String,
    eyes: String,
}

<snip>

    let flog: Flog = animal_new(&"Pyoko".to_string(), &"Black".to_string());

<snip>
   Compiling playground v0.0.1 (/playground)
error[E0277]: the trait bound `Flog: Animal` is not satisfied
  --> src/main.rs:57:22
   |
57 |     let flog: Flog = animal_new(&"Pyoko".to_string(), &"Black".to_string());
   |                      ^^^^^^^^^^ the trait `Animal` is not implemented for `Flog`
   |

Rust Playground

こうして型安全に初期化されたら、実際に鳴くときもジェネリクス関数を使えば、違う型の違う実装の、しかし同じ名前のメソッドが使える という訳だ。

fn animal_cry<T: Animal>(animal: T, cry_str: String) {
    animal.cry(cry_str);
}

<snip>

    animal_cry(cat, "myaw".to_string());  //-> Cat, Black eyes JiJi say, myaw
    animal_cry(dog, "bowow".to_string());  //-> Dog, Red eyes Mugi say, bowow

<snip>

感想

ジェネリクスとトレイトを使い回せればかなりコードを簡略化してかけそう😝