😺

Rustにおける関数ポインタやクロージャ

2021/01/27に公開

Rustにおける関数ポインタやクロージャの扱いを整理する

Rustにおける関数ポインタ

fnというキーワードを用いると関数ポインタ型を指すことができる。

https://doc.rust-lang.org/reference/types/function-pointer.html

これを利用することで以下のようなコードが書ける

main.rs
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));
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e2582ea93148b2bc9f391c3357bd180e

ここで注意するのはfuncの型は正確には関数ポインタ型ではなく、関数定義型(Function item types)という型になっている。

https://doc.rust-lang.org/reference/types/function-item.html

上の例でfuncapply_twiceの引数に取れるのは関数定義型から関数ポインタ型への型強制が行われているからである。

https://doc.rust-lang.org/reference/type-coercions.html

関数定義型は型としての情報に関数の引数や返り値の型のみならず、関数名などの情報も含まれていて、その関数ただ1つを指すようになっている。

関数ポインタ型はunsafeキーワードによる安全性の情報やexternのようなABIの情報も含まれる。

Fnなどのトレートとの違い

fnによる関数ポインタ型はそれ自体で型であるが、FnFnOnceFnMutはトレートなので、それ自体は型でない。よって完全に別物である。
つまり

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))
}

関数定義型はFnFnOnceFnMutを実装しているので、apply_twiceを上のように置き換えても先程のコードは動く(dynを使う場合はfuncを参照として渡す)。
関数ポインタ型もsafeな場合に限り、これらのトレートを実装している。

クロージャ型

クロージャ表現によりつくられるクロージャはクロージャ型というこれまた別の型を持っていて、関数ポインタ型や関数定義型とは違う。

main.rs

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)
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=41b2252a7432e75757974225b4b5d951

先程の例とは違い、FnではなくFnOnceを要求している点に注意。Fnは複数回よべる関数である必要があるが、f2moveを使っていくつかの値の所有権を奪っている。
そのため、複数回呼び出しすると所有権を複数回奪うことになるのでそもそも複数回呼べない。
また、Fnはキャプチャーした環境に対して変更を行うことができない。f3は内部でaに対してpushを呼び出しているが、このためにはaへのミュータブルな参照を取る必要がある。よってこちらもFnは実装できず、FnMutが実装されている。
FnOnceFnMutFnが実装された型であれば必ず実装されているため、すべてのクロージャを取ることができる(FnMutFnFnOnceのサブトレートである)。
また、Fnが実装されていればFnMutが実装されている(FnFnMutのサブとレートである)。

おまけ

関数定義型は関数名を型情報の中に含む。そのため、std::any::type_nameをつかって型を出力すると関数名を出力できたりする。
実行例は以下の通り

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=838c51e638483f652bfa7ff7d4ba614d

参考資料

Discussion