最近Rustを触り始めて、クロージャをOption型にした時の扱い方で詰まったので、その辺りを色々といじくり回してみました。
おそらく、今後Rustを扱う場合にも同様のことで詰まりそうなので、残しておきます。
今回の投稿は、クロージャの型についての調査です。Rustの用語周りについて正確に把握していなため、変な部分があるかもしれません。
※Rustのバージョンは1.24.0 (stable)です。
クロージャの型指定
このような関数にクロージャを渡すことを考えます。
fn exec_opt_fnonce<T, U, F>(a: T, opt: Option<F>) -> U
where U: Default, F: FnOnce(T) -> U {
match opt {
Some(f) => f(a),
None => Default::default()
}
}
上記の関数は、Someの場合は、引数aをクロージャの引数に渡して、クロージャの戻り値をそのまま返し、Noneの場合は初期値を返す関数です。
※DefaultトレイトはDefault::default()で初期値を返せるトレイトです。
std::default::Default - Rust
実装してみる
この関数を実行するコードを作成します。
println!("exec_opt_fnonce Some : {}", exec_opt_fnonce(1, Some(|i| i + 1)));
実行結果は
exec_opt_fnonce Some : 2
問題なく動きます。では、SomeではなくNoneを渡す場合はどうでしょう。
println!("exec_opt_fnonce None : {}", exec_opt_fnonce(1, None));
コンパイルが通りません。「型がわからねえよ!」と怒られます。
では、適当にキャストします。
println!("exec_opt_fnonce None : {}", exec_opt_fnonce(1, None as Option<FnOnce(i32) -> i32>));
コンパイルが通りません。「サイズがわからねえよ!」と怒られます。変数や引数はサイズが決まっていないといけないようです。
サイズ不定形
実際にクロージャを作るわけではないのに、どうやってサイズを決められるのでしょうか?
ここが詰まったポイントだったのですが、「サイズ不定型でも、その参照はサイズが決まっている」ということで、クロージャを参照にしてみます。
println!("exec_opt_fnonce None : {}", exec_opt_fnonce(1, None as Option<&FnOnce(i32) -> i32>));
進展が有りましたが、まだコンパイルは通りません。以下のエラーメッセージが表示されます。
error[E0277]: the trait bound `std::ops::FnOnce(i32) -> i32: std::ops::Fn<(i32,)>` is not satisfied
指定すべき型は、FnOnceではなくFnなのでしょうか?
println!("exec_opt_fnonce None : {}", exec_opt_fnonce(1, None as Option<&Fn(i32) -> i32>));
無事に通りました。実行結果も想定どおり初期値が返ってきています。
exec_opt_fnonce None : 0
どうやら、Fnの参照を指定すると良いようです。
疑問点
無事想定通りの動作となりましたが、いくつか疑問も残っています。
- ジェネリックの境界で
FnOnceを指定しているのに、なぜFnを渡さなければいけないのか。 - ジェネリックの境界で参照を指定してないのに、なぜ参照(
&Fn(i32) -> i32)を渡せるのか。
1の疑問については、下記のリンクを参考に推測してみました。
トレイトオブジェクト
Rustのクロージャ3種を作って理解する | κeenのHappy Hacκing Blog
FnOnceはトレイトのメソッドでSelfを使用しているため、トレイトオブジェクトになれない。
Fnはトレイトのメソッドで&Selfを使用しているため、トレイトオブジェクトになれる。
FnOnceとFnは、FnOnce⊃Fnの関係なので、&FnならFnOnceの条件でも渡せる…ということなのでしょうか?
これに関してはただの推測なので、鵜呑みにしないでください。
2の疑問については、追加で実験してみました。
let f = |i:i32| i;
//let a: Option<&Fn(i32) -> i32> = Some(f); // エラー Some(&f)ならOK
let b: Option<&Fn(i32) -> i32> = None; // OK
exec_opt_fnonce(1, Some(&f)); // OK
exec_opt_fnonce(1, Some(f)); // OK
exec_opt_fnonce(1, b); // OK
上の結果を見るに、&FnとFnは別の型だが、境界条件のFnOnceはFnも&Fnも通すということみたいですね。(境界条件FnMut、Fnも同様)
これ以上追うと泥沼にはまりそうなので、ひとまず「そういうもの」として納得しておくことにします。
疑問の解答 ※追記(2018/02/19)
コメントで疑問に答えていただきましたので、追記します。
Trait std::ops::FnOnce - Rust
impl<'a, A, F> FnOnce<A> for &'a F where
F: Fn<A> + ?Sized, type Output = <F as FnOnce<A>>::Output;
FnOnceトレイトが上記のように&Fnに対して実装されているため、&Fnは指定できる、&FnOnceに対しては無いため指定できないという理由みたいです。
ちなみに、FnOnceは&mut FnMutに対しても実装があるため
exec_opt_fnonce(1, None as Option<&mut FnMut(i32) -> i32>)
でもエラーにはならないですね。
Option<&F>、Option<&mut F>の場合
クロージャの参照やミュータブルな参照が要求されている場合も同様に
// Option<&F> where F : Fn(i32) -> i32
None as Option<&&Fn(i32) -> i32>
// Option<&mut F> where F : FnMut(i32) -> i32
None as Option<&mut &Fn(i32) -> i32>
のように、参照の参照にすることでNoneを作ることができます。
おそらく、Option以外の列挙型に関しても同様でしょう。
プリミティブ型 fn ※追記(2018/02/20)
コメントで教えていただいたことを追記します。
クロージャ(関数ポインタ)のプロミティブ型はfnとのことです。
関数ポインタのプロミティブ型fnは、unsafeではない場合はFn``FnMut``FnOnceを実装しているので、以下のように書いても問題ないです。
exec_opt_fnonce(1, None as Option<fn(i32) -> i32>);
クロージャのプリミティブ型がわかっていれば、難しく考えなくてもよかったですね…。こちらのほうが型を指定するだけなので、考え方がシンプルです。
考え方は前述の&Fnの例と同じでした。今回挙げた例のように型はとりえあずなんでもいい場合は、&Fnかfnを指定すれば問題なさそうです。
また、以下の書き方も出来ます。
exec_opt_fnonce(1, None::<fn(i32) -> i32>); // Option::None::<fn(i32) -> i32>
わからなかったのでキャストで書いていたのですが、ジェネリックの型もこう指定できるのですね。
勉強になりました。