LoginSignup
7
4

More than 5 years have passed since last update.

【Rust】クロージャのOption型でNoneを作る場合の型指定

Last updated at Posted at 2018-02-18

最近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の参照を指定すると良いようです。

疑問点

無事想定通りの動作となりましたが、いくつか疑問も残っています。

  1. ジェネリックの境界でFnOnceを指定しているのに、なぜFnを渡さなければいけないのか。
  2. ジェネリックの境界で参照を指定してないのに、なぜ参照(&Fn(i32) -> i32)を渡せるのか。

1の疑問については、下記のリンクを参考に推測してみました。
トレイトオブジェクト
Rustのクロージャ3種を作って理解する | κeenのHappy Hacκing Blog
FnOnceはトレイトのメソッドでSelfを使用しているため、トレイトオブジェクトになれない。
Fnはトレイトのメソッドで&Selfを使用しているため、トレイトオブジェクトになれる。
FnOnceFnは、FnOnceFnの関係なので、&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

上の結果を見るに、&FnFnは別の型だが、境界条件のFnOnceFn&Fnも通すということみたいですね。(境界条件FnMutFnも同様)

これ以上追うと泥沼にはまりそうなので、ひとまず「そういうもの」として納得しておくことにします。

疑問の解答 ※追記(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とのことです。

Primitive Type fn - Rust

関数ポインタのプロミティブ型fnは、unsafeではない場合はFnFnMutFnOnceを実装しているので、以下のように書いても問題ないです。

exec_opt_fnonce(1, None as Option<fn(i32) -> i32>);

クロージャのプリミティブ型がわかっていれば、難しく考えなくてもよかったですね…。こちらのほうが型を指定するだけなので、考え方がシンプルです。
考え方は前述の&Fnの例と同じでした。今回挙げた例のように型はとりえあずなんでもいい場合は、&Fnfnを指定すれば問題なさそうです。
また、以下の書き方も出来ます。

exec_opt_fnonce(1, None::<fn(i32) -> i32>); // Option::None::<fn(i32) -> i32>

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

7
4
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4