この記事は
筆者がRustのTraitを †完全に理解† したので、象に鳥トレイトを付けてダンボにしようという記事です。
ようするに
RustにおけるTraitの意味について、自分なりに噛み砕いて説明します。
Traitとは?
RustにおけるTraitは、「〇〇の仲間」であることを表します。
とあるAが「〇〇の仲間」であるためには、Traitの名の通り、〇〇に共通する特性をAが有している必要があります。
これを、Rustでは以下のように書きます。
trait Bird {
fn fly(&self);
}
これは鳥トレイトですが、ようするに、「飛べば(fly関数を実装していれば)鳥の仲間だ(暴論)」ということです。
この時、別に飛び方はどんなものでも構わないので、その関数の中身は書かなくて大丈夫です。
Traitの実装(impl)
今までの話的に、「鳥」=>「飛ぶ」なので、今現在飛んでいない生き物にfly関数を実装してやれば、その生き物は鳥の仲間であるということができそうです。(論理学的には怒られそうですが…)
その作業は以下のように書きます。
struct Chicken {
// くちばしの大きさ
beak: usize,
// 体の側面に付いているものの名称
wing: String,
}
impl Bird for Chicken {
fn fly(&self) {
println!("{}をパタパタ", &self.wing);
}
}
鶏は、くちばしの大きさと側面に付いているものの名称がわかれば定義できます(暴論)。
しかし、これだけでは鳥の仲間にはなれません。
鳥の仲間になるためにはBirdトレイトを実装していなければならないので、implでChickenへの実装を行います。
この時、Birdトレイトの中身(fly関数)を記述する必要があります。
そして無事に、鶏は鳥の仲間となることができました。
型ごとに関数の実装を変える
同様のことを、象に対してもやってみましょう。
まず、象は鼻の長さと顔の側面に付いているものの名称がわかれば定義できます(暴論)。
struct Elephant {
// 鼻の長さ
trunk: usize,
// 顔の側面に付いているものの名称
ear: String,
}
そしてBirdトレイトを実装することで、晴れて象は鳥の仲間となることができました。
impl Bird for Elephant {
fn fly(&self) {
println!("{}をパタパタ", &self.ear);
}
}
鶏には羽があったので「羽(self.wing)をパタパタ」することで飛びましたが、象にはそれが無いので、「耳(self.ear)をパタパタ」することで飛んでもらっています。
このように、共通のトレイトを持つためには共通の関数を持っている必要があるのですが、その関数の内容は、トレイトを実装する型ごとに変えることができるのですね!
すべての型に共通する関数を作る(default)
実は、「鳥の仲間」には、飛ぶこと以外にも重要な性質があります。
それは…
「魚ではない」!
…ということです。
"もし魚だったら木の下に埋めてもらっても構わないよ!"
というわけで、この性質もBirdトレイトの関数で表現したいのですが、これは型ごとに異なる実装をする必要があるでしょうか?
むしろ、毎回Birdトレイトを実装する際に、「いやいや魚じゃないんだよ」と一々書くのは面倒くさそうです。
というわけで、こういう普遍的な関数はBirdトレイトの定義の中にdefaultを書くことができます。
trait Bird {
fn fly(&self);
fn is_fish(&self) -> bool {
false
}
}
つまり、こうすると、
// 再掲(さっきと同じ)
impl Bird for Chicken {
fn fly(&self) {
println!("{}をパタパタ", &self.wing);
}
}
fn main(){
let kfc = Chicken { beak: 1, wing: "羽".to_string() };
println!("{}", kfc.is_fish()); // false
}
のように、implで内容を記述せずともis_fish関数を使うことができます。
defaultの上書き
ところでみなさん、トビウオってご存じですか?
struct FlyingFish {
// 生臭さ
fishiness: usize,
// 体の側面に付いているものの名称
wing: String,
}
そうですね。このように、生臭さと体の側面に付いているものの名称によって定義できる生物です(もはや失礼)。
トビウオは立派な羽を持っていますので、Birdトレイトを実装しても良さそうです。
しかし…
うーん…
魚、なんですよねぇ…。
このような場合には、implの際にis_fishの中身を書いて、defaultを上書きしてやることができます。
impl Bird for FlyingFish {
fn fly(&self) {
println!("{}をパタパタ", &self.wing);
}
fn is_fish(&self) -> bool {
true
}
}
fn main(){
let cheepcheep = FlyingFish { fishiness: std::usize::MAX, wing: "羽".to_string() };
println!("{}", cheepcheep.is_fish()); // true
}
トレイト境界(trait bound)
最後に、ドーバー海峡を横断させる関数go_dover(animal)について考えてみましょう。
ドーバー海峡を横断するためには大体100回くらいパタパタする必要があるのですが、そのためには、関数の引数となるanimalはBirdトレイトを実装していなければなりません。
そうでない動物にgo_doverさせようとすると、動物愛護法で捕まってしまいます。
これは、関数定義に<T>のようなジェネリクスを導入することで表現できます。
fn go_dover<T: Bird>(animal: &T) {
for _ in 0..100 {
animal.fly();
}
}
この時、引数animalの型は、Birdトレイトを実装した任意の型Tとなります。
このように、個々の型ではなく「〇〇トレイトを実装しているか?」で指定した条件のことをトレイト境界(trait bound)と呼びます。
まとめ
- トレイトは「〇〇の仲間」を表す
- 「〇〇の仲間」が持つべき関数を備えることで、トレイトを型に実装することができる。
- トレイト境界を用いて、「〇〇トレイトを実装しているか?」という条件を指定することができる。
全コード
trait Bird {
fn fly(&self);
fn is_fish(&self) -> bool {
false
}
}
struct Chicken {
// くちばしの大きさ
beak: usize,
// 体の側面に付いているものの名称
wing: String,
}
struct Elephant {
// 鼻の長さ
trunk: usize,
// 顔の側面に付いているものの名称
ear: String,
}
struct FlyingFish {
// 生臭さ
fishiness: usize,
// 体の側面に付いているものの名称
wing: String,
}
struct Sponge {
// カニカーニでアルバイトしている奴か?
square_pants: bool,
}
impl Bird for Chicken {
fn fly(&self) {
println!("{}をパタパタ", &self.wing);
}
}
impl Bird for Elephant {
fn fly(&self) {
println!("{}をパタパタ", &self.ear);
}
}
impl Bird for FlyingFish {
fn fly(&self) {
println!("{}をパタパタ", &self.wing);
}
fn is_fish(&self) -> bool {
true
}
}
fn go_dover<T: Bird>(animal: &T) {
for _ in 0..100 {
animal.fly();
}
}
fn main(){
let kfc = Chicken { beak: 1, wing: "羽".to_string() };
kfc.fly(); // 羽をパタパタ
println!("{}", kfc.is_fish()); // false
let cheepcheep = FlyingFish { fishiness: std::usize::MAX, wing: "羽".to_string() };
println!("{}", cheepcheep.is_fish()); // true
let dumbo = Elephant { trunk: 1, ear: "耳".to_string() };
go_dover(&dumbo); // 羽をパタパタ x 100
let bob = Sponge { square_pants: true };
// go_dover(&bob); // エラー! the trait `Bird` is not implemented for `Sponge`
}