業務で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 なら割と簡単に記述できるんだけど速度がなぁ~
-> ん!? 静的ディスパッチで返却できるじゃん! しかもクロージャとの相性抜群!
って感じ?
以上!