Rustでのゼロコスト抽象化の概念に触れる。(traitとジェネリクス)

2021-11-23

はじめに

勤労感謝の日なのですが、弊社は車カレンダーで動いているので平日扱いとなります。 非人道的カレンダーなのです。みなさんが勤労に感謝しているときに、私は労働に感謝するわけです。 感謝。労働できることに感謝しよう。

僕、なんというか勤労なんだよ。感謝してよ。感謝の印に休日をくれよ。頼むよ。

いわゆる勤労な祝日のはずなのに平日扱いで働くような人材、いわばゼロコスト人材なのです。 というわけでRustのゼロコスト抽象化について学びましょう。

ゼロコスト抽象化

大小問わず、ソフトウェアを作るとき実装上扱いやすくするために、抽象化を行っています。 内部データと処理を一つのクラスにまとめて、外部からは細かい処理のことを気にしないで良いように作るようなカプセル化。 オブジェクト間でI/Fを設けて使い方、使われ方を共通化してオブジェクトを扱いやすくする手法をポリモーフィズムと呼びます。

そのような抽象化の実装パターンがいわゆるデザインパターンです。 こうした抽象化は良い側面が多いのも事実なのですが、抽象化されたコードを実行する際に支払うコストが増加します。 ここでいうコストというのが、メモリの使用量であったり処理速度を指します。

他の言語であれば、抽象化をおこない実行するときには動的にメモリを割り当てて、実行中にメモリの解放と割当を行っています。 Rustの思想は、コンパイル時に解決しておき実行時にはゼロコストとなるように、言語や標準ライブラリが設計されているのです。 例えば、トレイトやジェネリックスを使用できる型への変換は、コンパイル時にまとめて行うことで実行にかかるコストをゼロにするなどがあたります。

システムリソースを使用する際には、強い制約が課せられているため、慣れるまでは何で?って思うようなエラーによく遭遇しますが、そうした背景からくるものなのです。

trait

The Rust Programming Language 日本語版 トレイト: 共通の振る舞いを定義する

他の言語ではClassを使ってポリモーフィズムを実装することになります。 Interfaceがよく似た機能であると記載があります。 traitを使って、共通的な関数を定義しましょう、

struct Gun;
struct Assalt;

trait Fire {
    fn fire(&self);
}

impl Fire for Gun {
    fn fire(&self) {
        println!("Gun fire");
    }
}

impl Fire for Assalt {
    fn fire(&self) {
        println!("Assalt fire");
    }
}

fn main() {
    let gun = Gun {};
    gun.fire();

    let assalt = Assalt {};
    assalt.fire();
}

実行結果

同じ関数名で呼び出しているけど振る舞いを変えることが出来ましたね。

cargo run
   Compiling hello-learn v0.1.0 (E:\SourceCode\Rust\hello-learn)
    Finished dev [unoptimized + debuginfo] target(s) in 0.95s
     Running `target\debug\hello-learn.exe`
Gun fire
Assalt fire

ジェネリクス

C++で見かけるtemplate構文みたいな機能です。 書き方はと書くのでほんと大体同じです。

軽く説明すると、型が違う同じ処理をしたいときに、それぞれの型で関数を作るのではなく 型を明示的に記載せずに、コンパイラにまかせて実行するような使い方をします。 Rustではコンパイル時点で、取りうる組み合わせの数だけmaketuple関数に展開しています。

fn maketuple<T, S>(t: T, s: S) -> (T, S) {
    print!("型名称:{}  ", std::any::type_name::<T>());
    (t, s)
}

fn main() {
  let t1 = maketuple(1, 2);
  println!("{},{}", t1.0, t1.1);

  let t2 = maketuple("TIN", "POKO");
  println!("{},{}", t2.0, t2.1);

  let t3 = maketuple(vec![1, 2, 3, 4, 5, 6, 7], vec![8, 9, 10]);
  println!("{:?},{:?}", t3.0, t3.1);
}

実行結果

型名と、入力した戻り値を出力しています。 Vector方はprintln!で出力する場合、{:?}と書く必要があります。

Finished dev [unoptimized + debuginfo] target(s) in 0.72s
 Running `target\debug\hello-learn.exe`
型名称:i32  1,2
型名称:&str  TIN,POKO
型名称:alloc::vec::Vec<i32>  [1, 2, 3, 4, 5, 6, 7],[8, 9, 10]

まとめ

Rustっていうとゼロコスト抽象化がすごいんだよみたいな話が聞こえてきて何だろうなって思ってのですが ようは、メモリ容量とか実行速度とかをなるべき犠牲にしないようにするためのサポート機能が充実してるんですよって話と理解した。

この調子でやっていこうな。