LoginSignup
2
1

Rust Impl Traitとクロージャの返却について

Posted at

業務でRustを触っており, impl Trait とやらに遭遇しました.
初見では意味がわからなかったのですが,
順序立てて理解していくと, 頭にすーっと入っていったので共有します。

自己満(腑に落ちる) > 正しい の記事なので, 言葉の使い方が間違っているところも大目に見てください. + 後半になるにつれて雑になります..

※ 記述中. 今後修正していきます🙇

目次

  • 抽象化を行う方法
  • 具体例(引数での利用)
  • 返り値での利用(引数の時と違って...)

抽象化を行う方法

抽象化については -> https://qiita.com/aaaoooiii/items/68386f497ee0c73222c2

抽象化を行う方法として

  • 静的ディスパッチを使った方法 => トレイト境界 + ジェネリクスを利用
  • 動的ディスパッチを使った方法 => トレイトオブジェクトを利用

が用意されている(※他にもある)
Rustでは 基本的に, 静的ディスパッチを採用. ※ ゼロコスト抽象化などが関わってくる.

具体例

std::fmt::Displayトレイトを満たしている型を引数にとり, 標準出力する関数を考える.

静的ディスパッチ

静的ディスパッチを使った方法では, 以下のようにトレイト境界 + ジェネリクスを利用

fn print<T: std::fmt::Display>(arg: T) {
    println!("{}", arg);
}

コンパイル時に, 型ごとの関数が内部的に作成される(単相化)

// i32型用のprint
fn print_i32(arg: i32) {
    println!("{}", arg);
}

// String型用のprint
fn print_string(arg: String) {
    println!("{}", arg);
}

コンパイル時に内部的に関数を作成しているので速い... ここ後から書く

動的ディスパッチ

動的ディスパッチを使った方法では, トレイトオブジェクトを利用.

fn print(arg: Box<dyn std::fmt::Display>) {
    // メソッドの実行は略.. とりあえずこう書けるよってところだけ
    println!("{}", arg);
}

この例ではメソッドを実行していないが, メソッドを実行する場合に
トレイトオブジェクト -> 仮想関数テーブル -> 具象型のメソッド -> 実行
といった手順を踏んでいるのでオーバヘッドが発生して, 少し遅くなる.

詳しくは -> https://qiita.com/aaaoooiii/items/68386f497ee0c73222c2

返り値での利用

次は, 関数の返り値に抽象化を利用する方法について,
i32型の値を引数にとり, そのまま返却する関数を考える

静的ディスパッチ

まずは引数の時と同様に静的ディスパッチから

fn print<T: std::fmt::Display>(num: i32) -> T {
    match num {
        0 => 1 as i32,
        _ => "hello".to_string(),
    }
}

これでいいかと思いきや, これでは コンパイルエラー を起こす.
なぜか -> 関数の返り値はスタック上のサイズを知っている必要があるから.
言葉でもよく分からない場合のために.. またまたまたまた具体例. どんな時に矛盾が生じるか

静的ディスパッチなので, 内部的に型ごとの関数を作成しているはず. その時のことを考えると...
※ ここから先は, 矛盾を出すためだけのコードなので間違っているかも..何ですけど許してください.

// i32型用のprint
fn print_i32(num: i32) -> i32 {
    match num {
        0 => 1 as i32,
        _ => "hello".to_string(),
    }
}

// String型用のprint
fn print_string(num: i32) -> String {
    match num {
        0 => 1 as i32,
        _ => "hello".to_string(),
    }
}

この時点でおかしい. 例えはprint_stringでは返り値の型にString型を指定しているのに, i32型を返却する可能性がある.

ってことで, 静的ディスパッチを使って返り値を返却するのは無理っぽい(※ 無理じゃない. 後述)

動的ディスパッチ

次に動的ディスパッチを利用した場合は,

fn print(num: i32) -> Box<dyn std::fmt::Display> {
    match num {
        0 => Box::new(1 as i32),
        _ => Box::new("hello".to_string()),
    }
}

こっちは, コンパイルできる!!

でも... 静的ディスパッチしたい.. 動的ディスパッチは遅くなるし..

ってことで impl Trait

Impl Trait

まず何が嬉しいのか
=> 静的ディスパッチでトレイト(抽象型)を返すことができる.
ただし条件あり
返り値の型が1つに定まる必要がある.

fn print(num: i32) -> impl std::fmt::Display {
    match num {
        0 => Box::new(1 as i32),
        _ => Box::new(2 as i32),
    }
}

これならいいってこと.
ここで疑問..
ならこれで良くないか?

fn print(num: i32) -> i32 {
    match num {
        0 => Box::new(1 as i32),
        _ => Box::new(2 as i32),
    }
}

その通り. これでいい(笑)
だけどこの impl Trai を使うことで嬉しいタイミングがある
それは, クロージャを返却するとき. (※クロージャについての詳しい説明は他の記事で)

ただし, クロージャの型は分からない ってところだけは必要なので説明

クロージャと型

例えば, こんなクロージャを定義したとして, この型分かるやついる?! いねえよな!

let c: ? = |arg| {
    arg + x
};

ってこと. 真面目に言うと 匿名構造体 とやら
でクロージャ自体の型は分からないけど,
FnOnce, FnMut, Fn いずれかのトレイトは実装している.
↑ クロージャ3種盛りの話は違う記事で

完成系

で, 何らかのクロージャを返却する関数を考える.

fn count(n: u32) -> impl FnMut() -> u32 {
    let mut num = n;
    move || {num += 1; num}
}

ちょっと分かりづらいけど, FnMut() -> u32 <-これがトレイト
しっかり, 静的ディスパッチでトレイトを返却している
これで完成

補足

勿論, 動的ディスパッチでクロージャを返却することもできる.

fn count(n: u32) -> Box<dyn FnMut() -> u32> {
    let mut num = n;
    Box::new(move || {num += 1; num})
}

ただ, 基本的には静的ディスパッチをしたいので, この書き方はしないよね( impl Trait が出てくるまではこっちが主流)って話..

まとめ

抽象化プログラミングしたいな~
-> 引数の抽象化は簡単だけど, 返り値の抽象化だるいな~
-> 動的ディスパッチ Box なら割と簡単に記述できるんだけど速度がなぁ~
-> ん!? 静的ディスパッチで返却できるじゃん! しかもクロージャとの相性抜群!

って感じ?
以上!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1