最近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>
わからなかったのでキャストで書いていたのですが、ジェネリックの型もこう指定できるのですね。
勉強になりました。