やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

RustのOOP(トレイトオブジェクト)

 Vec<Box<MyTrait>>とすることでtrait(インタフェース)を実装した型を受け入れる。

成果物

異なる型を許容する

 たとえばGUIライブラリを作るとき、drawメソッドを持った異なるコンポーネントを作りたいとする。将来、新たなコンポーネントを作りうる。

  • Component
    • Button
    • TextField
    • Image
    • SelectBox
component.draw();

 componentButtonTextFieldなど様々な型でありうる。

$ cargo new gui --lib

一般的な振る舞いをトレイトで定義する

 トレイトオブジェクト。

src/lib.rs

pub trait Draw {
    fn draw(&self);
}

 トレイトオブジェクトは、トレイトを実装する型のインスタンスを指す。Box<T>などのポインタで指定する必要がある。

pub struct Screen {
    pub components: Vec<Box<Draw>>,
}
impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

 トレイト境界を用いると以下のように書ける。

pub struct Screen<T: Draw> {
    pub components: Vec<T>,
}
impl<T> Screen<T>
    where T: Draw {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

 これにてBox<Button>Box<TextField>を含むVec<T>を保持できる。

Button

pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}
impl Draw for Button {
    fn draw(&self) {
        // code to actually draw a button
        // 実際にボタンを描画するコード
    }
}

src/lib.rs

pub trait Draw {
    fn draw(&self);
}
pub struct Screen<T: Draw> {
    pub components: Vec<T>,
}
impl<T> Screen<T>
    where T: Draw {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}
#[derive(Debug)]
pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}
impl Draw for Button {
    fn draw(&self) {
        println!("{:?}", self);
    }
}

src/main.rs

use gui::{Screen,Button};
use gui::Draw;
#[derive(Debug)]
struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}
impl Draw for SelectBox {
    fn draw(&self) {
        println!("{:?}", self);
    }
}
fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No")
                ],
            }),
            Box::new(Button {
                width: 50,
                height: 10,
                label: String::from("OK"),
            }),
        ],
    };
    screen.run();
}
$ cargo run

 コンパイルできない……。ドキュメントと何が違うの?

$ cargo run
...
error[E0308]: mismatched types
  --> src/main.rs:26:22
   |
26 |               Box::new(Button {
   |  ______________________^
27 | |                 width: 50,
28 | |                 height: 10,
29 | |                 label: String::from("OK"),
30 | |             }),
   | |_____________^ expected struct `SelectBox`, found struct `gui::Button`
   |
   = note: expected type `SelectBox`
              found type `gui::Button`

error[E0277]: the trait bound `std::boxed::Box<SelectBox>: gui::Draw` is not satisfied
  --> src/main.rs:15:18
   |
15 |     let screen = Screen {
   |                  ^^^^^^ the trait `gui::Draw` is not implemented for `std::boxed::Box<SelectBox>`
   |
   = note: required by `gui::Screen`

error[E0599]: no method named `run` found for type `gui::Screen<std::boxed::Box<SelectBox>>` in the current scope
  --> src/main.rs:33:12
   |
33 |     screen.run();
   |            ^^^
   |
   = note: the method `run` exists but the following trait bounds were not satisfied:
           `std::boxed::Box<SelectBox> : gui::Draw`

 やはりVec型は最初の型と同じものしか受け付けないじゃないか……騙された。トレイトオブジェクトなら解決できるんじゃなかったの? トレイトオブジェクトとは一体……。それを学習する項目なのに、成功するコードがないから何もわからないまま……。

Draw未実装インスタンスを追加するとエラー

error[E0277]: the trait bound `std::boxed::Box<std::string::String>: gui::Draw` is not satisfied
 --> src/main.rs:4:18
  |
4 |     let screen = Screen {
  |                  ^^^^^^ the trait `gui::Draw` is not implemented for `std::boxed::Box<std::string::String>`
  |
  = note: required by `gui::Screen`

 これは期待通り。

できた

 ドキュメントのコード紛らわしい。完全なコードを残しておく。

$ cargo new gui --lib

src/lib.rs

pub trait Draw { fn draw(&self); }
pub struct Screen {
    pub components: Vec<Box<Draw>>,
}
impl Screen {
    pub fn run(&self) {
        for c in self.components.iter() { c.draw(); }
    }
}
#[derive(Debug)]
pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}
impl Draw for Button {
    fn draw(&self) { println!("Button: {:?}", self); }
}

src/main.rs

use gui::{Screen,Button,Draw};
#[derive(Debug)]
struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}
impl Draw for SelectBox {
    fn draw(&self) { println!("SelectBox: {:?}", self); }
}
fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No")
                ],
            }),
            Box::new(Button {
                width: 50,
                height: 10,
                label: String::from("OK"),
            }),
        ],
    };
    screen.run();
}
$ cargo run
...
SelectBox: SelectBox { width: 75, height: 10, options: ["Yes", "Maybe", "No"] }
Button: Button { width: 50, height: 10, label: "OK" }

 ポイントはトレイト定義pub components: Vec<Box<Draw>>,のところ。Vecの要素の型はtraitのポインタらしい。

 なんか直感的じゃない。呼出元ではVecの要素に、traitであるDrawのメソッドを実装したButton, SelectBoxを渡している。それらはコンポーネントの具体名であり、Drawを匂わせるものではない。オブジェクト指向の継承のほうがわかりやすい。

所感

 ポリモーフィズム多態性)ってことだよね?

 「ポリモーフィズム多態性=多様性=多相性」ってことらしい。表記ゆれしすぎ。英語ならpolymorphism

 これをRustのトレイト境界にて実現する方法を「パラメータ境界多相性(bounded parametric polymorphism)」と呼ぶってことか?

 そもそもトレイト境界もよくわからん。where T: Clone, Debugとかのヤツじゃないの? ジェネリックで何でも許しておいて絞り込むヤツ。今回のコードも<>使ってるからジェネリックであり、トレイトで絞り込んでるっぽいからトレイト境界ってことなの?

 C#言語などの継承はクラス単位の多相性。でもRust言語のパラメータ境界多相性はメソッド単位の多相性。Rustのほうが無駄なメソッドを継承せずに済むし、厳密に絞りこめる。でも処理をまとめたいとき、膨大な数のトレイト境界を書くハメになりそう。

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1219 SMP Tue May 14 21:20:58 BST 2019 armv7l GNU/Linux

前回まで