Rustにおける関数ポインタやクロージャ
Rustにおける関数ポインタやクロージャの扱いを整理する
Rustにおける関数ポインタ
fnというキーワードを用いると関数ポインタ型を指すことができる。
これを利用することで以下のようなコードが書ける
fn func(x: i32) -> i32 {
x * x
}
fn apply_twice(x: i32, f: fn(i32) -> i32) -> i32 {
f(f(x))
}
fn main() {
println!("{}", apply_twice(3, func));
}
ここで注意するのはfuncの型は正確には関数ポインタ型ではなく、関数定義型(Function item types)という型になっている。
上の例でfuncがapply_twiceの引数に取れるのは関数定義型から関数ポインタ型への型強制が行われているからである。
関数定義型は型としての情報に関数の引数や返り値の型のみならず、関数名などの情報も含まれていて、その関数ただ1つを指すようになっている。
関数ポインタ型はunsafeキーワードによる安全性の情報やexternのようなABIの情報も含まれる。
Fnなどのトレートとの違い
fnによる関数ポインタ型はそれ自体で型であるが、Fn・FnOnce・FnMutはトレートなので、それ自体は型でない。よって完全に別物である。
つまり
fn apply_twice(x: i32, f: Fn(i32) -> i32) -> i32 {
f(f(x))
}
というようには書けない。このようなことをしたい場合は、ジェネリクスを使いFnを実装した型を要求するという形にするか、dynキーワードを使う。
ジェネリクスの場合は、
fn apply_twice<F>(x: i32, f: F) -> i32 where F: Fn(i32) -> i32 {
f(f(x))
}
dynキーワードの場合は
fn apply_twice(x: i32, f: &(dyn Fn(i32) -> i32)) -> i32 {
f(f(x))
}
関数定義型はFn・FnOnce・FnMutを実装しているので、apply_twiceを上のように置き換えても先程のコードは動く(dynを使う場合はfuncを参照として渡す)。
関数ポインタ型もsafeな場合に限り、これらのトレートを実装している。
クロージャ型
クロージャ表現によりつくられるクロージャはクロージャ型というこれまた別の型を持っていて、関数ポインタ型や関数定義型とは違う。
fn debug_call<F>(f: F) where F: FnOnce() -> String {
println!("{}", f());
}
fn main() {
let mut a = String::from("");
let b = String::from("moved");
let f1 = || String::from("test");
let f2 = move || b;
let f3 = || { a.push('a'); String::from("modified") };
debug_call(f1);
debug_call(f2);
debug_call(f3);
println!("{}", a)
}
先程の例とは違い、FnではなくFnOnceを要求している点に注意。Fnは複数回よべる関数である必要があるが、f2はmoveを使っていくつかの値の所有権を奪っている。
そのため、複数回呼び出しすると所有権を複数回奪うことになるのでそもそも複数回呼べない。
また、Fnはキャプチャーした環境に対して変更を行うことができない。f3は内部でaに対してpushを呼び出しているが、このためにはaへのミュータブルな参照を取る必要がある。よってこちらもFnは実装できず、FnMutが実装されている。
FnOnceはFnMutとFnが実装された型であれば必ず実装されているため、すべてのクロージャを取ることができる(FnMutとFnはFnOnceのサブトレートである)。
また、Fnが実装されていればFnMutが実装されている(FnはFnMutのサブとレートである)。
おまけ
関数定義型は関数名を型情報の中に含む。そのため、std::any::type_nameをつかって型を出力すると関数名を出力できたりする。
実行例は以下の通り
Discussion