Rust のクロージャの環境, Fn* トレイト
クロージャ
クロージャは環境をもつ無名の関数である. 環境はそのクロージャの実行に必要なデータをまとめたものだ. C++ ではラムダ式と言われていて, 関数オブジェクトの糖衣構文である.
Rust のクロージャも C++ と同様に構造体を使って実現される. つまり構造体のメンバとして必要なデータ (=環境) を持ち, その構造体のメソッドとしてクロージャの処理本体を記述する. そう, 別にクロージャは特別な存在ではない.
例えば次のクロージャは,
let x = 3; let closure = || println!("{}", x); closure();
次のように実装できる.
struct ClosureType<'a> { x: &'a i32, } impl<'a> ClosureType<'a> { fn invoke(&self) { println!("{}", self.x); } } let x = 3; let closure = ClosureType { x: &x }; closure.invoke();
...しかし非常に面倒くさい. それでクロージャという形で簡易に使えるようになっているわけだ.
また, ここでは invoke()
というメソッドを普通に実行しているが, クロージャは関数呼び出し構文 ()
で実行できる. 実はこれは C++ 同様 ()
オペレータのオーバーロードである. Rust では, ()
のオーバーロードを後半に説明する Fn* 系のトレイトを実装することで行う. unstable なため nightly でしか使えないが, これらのトレイトを使うとクロージャ同様呼び出せるようになる.
#![feature(unboxed_closures, fn_traits)] struct ClosureType<'a> { x: &'a i32, } impl<'a> std::ops::FnOnce<()> for ClosureType<'a> { type Output = (); extern "rust-call" fn call_once(self, _: ()) { println!("{}", self.x); } } impl<'a> std::ops::FnMut<()> for ClosureType<'a> { extern "rust-call" fn call_mut(&mut self, _: ()) { println!("{}", self.x); } } impl<'a> std::ops::Fn<()> for ClosureType<'a> { extern "rust-call" fn call(&self, _: ()) { println!("{}", self.x); } } fn main() { let x = 3; let closure = ClosureType { x: &x }; closure(); }
この通り非常に面倒だが.
クロージャの環境
さて, クロージャは環境を持てる無名な関数だと説明し, 実装例も示した. ところで, ClosureType::x
の型が参照 &'a i32
になっていることに気がついただろうか?
それを説明する前に, クロージャの処理が環境を参照するまでに 2 ステップあることを確認しておきたい. これらそれぞれに, 次のように変数の取り方がある.
クロージャを作成したとき, 変数を環境にキャプチャする.
変数を環境内に & 借用する, &mut 借用する, 所有権ごともらいうける.
クロージャを実行するとき, 実行するメソッドに環境を渡す.
本体のメソッドが環境を & 借用する, &mut 借用する, 所有権をもらう.
上記は 2 点とも, クロージャ内部でのその変数の使い方によって自動的に決定する.
外部から環境へのキャプチャ
説明に使う構造体 S は次のように定義する.
struct S; impl S { fn ok_immutable(&self) {} // & 借用で OK fn need_mutable(&mut self) {} // &mut 借用が必要 fn need_ownership(self) {} // 所有権が必要 }
& 借用
特に必要性がない場合, 基本的には & 借用としてキャプチャされる.
let mut s = S; // ^^^ mut は一応 &mut 借用もできるように. 実際は不要 // 操作は & 借用でできる; s は & 借用 let closure = || s.ok_immutable(); { // 次は実際にコンパイルが通るので, // s は & 借用されていると示せる. // もし仮に s がムーブされていた // => ムーブ済み値の & 借用, コンパイルエラー // もし仮に s が &mut 借用されていた // => 複数の借用は存在できない, コンパイルエラー let _y = &s; } closure();
move クロージャを使うことで所有権のムーブ (Copy トレイトを実装していれば Copy) を強制することができる.
&mut 借用
値を変更する必要がある場合, &mut 借用としてキャプチャされる.
let mut s = S; // ^^^ mut は &mut 借用するので必要 // 操作に &mut が必要; s は &mut 借用 let mut closure = || s.need_mutable(); // ^^^ ここの mut は C++ に慣れていると // なぜ必要なのか分からない. // しかし Rust では必要. 詳しくは // 'inherited mutability' を調べるべし. // https://teratail.com/questions/114569 // に分かりやすい回答を頂いた. { // 次のコメントを外すと, 複数の借用は // 存在できない理由でコンパイルエラー // => s は &mut 借用されている // let _y = &s; } closure();
move クロージャを使うことで所有権のムーブ (Copy トレイトを実装していれば Copy) を強制することができる.
ムーブ
所有権を要求する操作をする場合, クロージャは所有権ごとムーブして取り込む. 本来はムーブする必要がないものでも, move クロージャを使うことでムーブ (Copy トレイトを実装していれば Copy) を強制することができる.
let mut s = S; // ^^^ mut は一応 &mut 借用もできるように. 実際は不要 // 操作に所有権が必要; s はムーブされた let closure = || s.need_ownership(); { // 次のコメントを外すと, ムーブされた値が // 使われた理由でコンパイルエラー // => s はムーブされている // let _y = &s; } closure(); // closure(); // エラー, closure はムーブ済み
move クロージャの例.
let mut s = S; // ^^^ mut は一応 &mut 借用もできるように. 実際は不要 // 操作自体は & 借用でできるが s は強制ムーブ let closure = move || s.ok_immutable(); { // 次のコメントを外すと, ムーブされた値が // 使われた理由でコンパイルエラー // => s はムーブされている // let _y = &s; } closure(); // クロージャ自体は Fn なので, 2 回目も問題ない. closure();
クロージャからメソッド本体への環境の借用または移動
次に, キャプチャされた環境からクロージャのメソッド本体に渡されるときについて考える. ここでどのように環境を渡すかということが Fn* トレイトにより分類されている.
なお, クロージャがどの Fn* トレイトを実装するかは処理本体に依存する. これは結局, 上で説明した「外部から環境へキャプチャするときにどうキャプチャされるか」とほとんど同じになる. 唯一違うのは move クロージャを要求するが処理本体では所有権を取得する必要がない場合である. (上の最後の move クロージャの例がまさにそれ. 処理本体では .ok_immutable()
以外は何もしないので. ) 他に例外がないのかと聞かれると自信はまったくない. ...なんか絶対ありそうな気もする.
Fn* トレイト
FnOnce, FnMut, Fn トレイトは, 関数呼び出し構文 func()
のオーバーロードに用いられるトレイトであった. クロージャはこれらのトレイトを実装した匿名型の実体ということになる. クロージャが匿名型なのはクロージャによって環境や処理が異なるため, 普遍的なクロージャの型というものが存在しえないからである. (ちなみに同一の内容のクロージャでも型は異なる. )
// 渡された f を arg に適用して, 結果を返す関数 fn apply<F>(f: F, arg: i32) -> i32 where F: FnOnce(i32) -> i32 { f(arg) }
Fn* トレイトには FnOnce, FnMut, Fn の三種類がある. この三種類には self
の取り方の違いがあり, それぞれ self
, &mut self
, &self
を取る. これらは次のように定義されていて, 継承関係がある.
pub trait Fn<Args> : FnMut<Args> { extern "rust-call" fn call(&self, args: Args) -> Self::Output; } pub trait FnMut<Args> : FnOnce<Args> { extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output; } pub trait FnOnce<Args> { type Output; extern "rust-call" fn call_once(self, args: Args) -> Self::Output; }
つまり, Fn トレイトを実装するには FnMut を実装しなければならないし, FnMut を実装するためには FnOnce を実装する必要があるということだ. 逆に, Fn トレイトが実装されていれば FnMut も FnOnce も実装されている.
Fn
Fn は環境を &self
としてイミュータブルな参照として受け取る. すなわち, クロージャ内部で環境の値を読み取るだけのような場合に実装される.
ローカル変数のリンゴをキャプチャしてその重さを計るクロージャ scale を考えよう. 次のようになる。
struct Apple { weight: u32, } let apple = Apple { weight: 3 }; let scale = || { println!("the weight of apple: {}", apple.weight); }; scale();
このクロージャは環境にリンゴ apple
を持つ. リンゴの重さを計るにはリンゴを改変する必要はない. だから scale の型は Fn トレイトを実装することができる (つまりそれが継承する FnMut も FnOnce も) .
Fn トレイトはなぜ FnMut を継承しているのか?
今はリンゴの重さを計りたい. これにはリンゴを改変する必要はない. だから「絶対つまみ食いをするなよ」と渡されても (Fn) 「リンゴをカットしてもいいよ」 (FnMut) と渡されても重さを計ることはできる. もしかしたらリンゴをカットした方が計りに載せやすい, ということはあるかもしれないが, そのままでも計れる. つまり FnMut は Fn より広い許可を与えることになり, Fn ならできたのに FnMut ではできない!ということにはならない.
FnMut
FnMut は環境を &mut self
としてミュータブルな参照として受け取る. すなわち, クロージャ内部で環境の値を変更する必要がある場合に用いられる.
先のようにリンゴをカットするのでもよいが, ちょっとコードにできないので, リンゴをちょっと食べたいという例にしよう. 「少しリンゴを貸してくれ, ちょっと食べたら返すから」と, こう言うわけだ.
struct Apple { weight: u32, } let mut apple = Apple { weight: 3 }; let mut eat = || { // 全体の 3 分の 1 をちょっとと言うか否かは別問題 apple.weight -= 1; println!("the weight of apple: {}", apple.weight); }; eat();
apple
を改変することなくしてリンゴを食べることはできないので, eat
の型は Fn は実装できない. 自動的に FnMut 止まりとなる.
FnMut トレイトはなぜ FnOnce を継承しているのか?
Fn トレイトが FnMut を継承する理由と同様に考えることができる. 「リンゴちょっとなら食べてもいいけど返してね」 (FnMut) と言われるだけでできるなら, 「リンゴをまるごとあげるよ」 (FnOnce) と言われてもできる (むしろ後者の方が全部食べれて嬉しいかもしれない) .
FnOnce
FnOnce は環境を self
として所有権ごともらいうける. 全てのクロージャが最低でも FnOnce は実装する. 実行できないクロージャは何なのか分からない.
食べた後のリンゴの種を回収して次のリンゴを育てたいとしよう (いや育たないだろうけど) . そのためには最後まで食べて種を回収する必要がある.
#[derive(Debug, Clone)] struct Seed; impl Seed { fn grow(self) -> Apple { Apple { weight: 4, seeds: vec![Seed; 3] } } } #[derive(Debug)] struct Apple { weight: u32, seeds: Vec<Seed> } let apple = Apple { weight: 3, seeds: vec![Seed; 4] }; let grow_apple = || { let seeds = apple.seeds.into_iter(); // 種回収 for seed in seeds { println!("{:?}", seed.grow()); // 育てる } }; grow_apple(); // 同じ種でもう一回育てようったって, そうはいかない // grow_apple();
grow_apple
内では種を回収している. そこまでするとリンゴの味見どころではない. そうするためにはリンゴごともらってしまう必要がある. 種回収しなきゃならないし. これでは FnOnce より先は実装できない.
なお FnOnce 関数は実行時に環境の所有権をとってしまうので二回目は実行できない. それが Once の由来だ.
まとめ
クロージャが環境に変数をキャプチャする方法には, & 借用, &mut 借用, 所有権ごとムーブの 3 通りがある. これらはクロージャの内部での使われ方により自動的に推論されるほか, move クロージャにすることでムーブを強制することもできる.
クロージャが実装するトレイトは 3 種類あり, 継承関係がある. どのトレイトまで実装するかはクロージャの内部での使われ方により自動的に推論される.