前々から思っていたのだが、Rustのクロージャという「名前」には違和感があった。なぜなら、クロージャと言うのは本来ただの関数のことを示すわけではないからだ。
だがクロージャについて調べてみて、改めて「Rustのクロージャは厳密にはクロージャではない」ということがはっきりしたので、今回はRustのクロージャはクロージャではないということを説明していこうと思う。
Rustの「クロージャ」とは
まずは簡単にRustの「クロージャ」(以下Rustクロージャ)について説明していこう。Rustクロージャは以下のように書くことで定義できる。
let closure = |arg| arg + 1i32;
Rustクロージャの定義は簡素過ぎて逆にわかりづらいので、一つずつ見ていこう。
- まず引数は
|
と|
の間に挟んで定義する。上の例だと、arg
が引数である。 - 引数を定義した後に式を定義する。上の例だと
arg + 1i32
がそれである。- Rustでは途中式がない場合
{}
が必要ない。もちろん途中式があったり、明示的に式を囲ったりする場合は{}
を使う。
- Rustでは途中式がない場合
2.より、明示的に{}
を加え、より関数定義らしく書いたのが以下のコードである。
let closure = |arg| { arg + 1i32 };
また、Rustクロージャを定義する際に、型注釈をすることも可能である。先ほどのコードに型注釈を加えてみる。
let closure = |arg: i32| -> i32 { arg + 1i32 };
なお、Rustクロージャの定義は関数定義とは違いジェネリクスやライフタイム注釈は使用できない。ただ、普通それが必要な用途で使われることはないので、これに関して困ることはないだろう。
普通の関数定義とRustクロージャの定義が違うのは、Rustクロージャの外側の変数を参照したり、所有権を関数内に移動したりできる(Rust的にはRustクロージャ内に所有権を移動させることを「所有権を奪う」と言うらしい)という点である。
たとえば、以下のコードはRustクロージャの外側の変数を参照している。
// 構造体の定義(理由は後述) #[derive(Debug)] struct Struct(i32); fn main() { let mut s = Struct(0i32); // Structのインスタンスを作成 // mut変数への破壊的代入のあるRustクロージャはmutが必要 let mut f = || { s.0 += 1i32; }; // Rustクロージャの実行 f(); println!("{s:?}"); // => Struct(1) }
注意として、mut
変数への破壊的代入のあるRustクロージャはmut
が必要という点がある。ただし、破壊的代入をしていなければたとえmut
変数だとしてもRustクロージャがmut
である必要はない。
先ほどの例は変数を参照していた。次に所有権を奪ってみる。
Rustクロージャで所有権を奪うには、Rustクロージャの定義の先頭にmove
を付ける。
#[derive(Debug)] struct Struct(i32); fn main() { let mut s = Struct(0i32); // 所有権を奪うためにはRustクロージャの定義の先頭にmoveを付ける let mut f = move || { s.0 += 1i32; println!("{s:?}"); }; // Rustクロージャの実行 f(); // WARNING: 所有権を奪われたので、変数sはもう使えない // s; }
Rust的に所有権を奪ったということは、以上の例で言うところの変数s
はもう使えないことを意味する。そのため所有権を奪われた後変数s
を使おうとするとコンパイルエラーになる。
ちなみにこれは、所有権の移動と意味的には同じである。そのため、先ほどのコードは以下のコードと等価である。
#[derive(Debug)] struct Struct(i32); fn main() { let s = Struct(0i32); let mut moved_s = s; // 所有権の移動 // 処理の実行 { moved_s.0 += 1i32; println!("{moved_s:?}"); } // WARNING: 所有権を奪われたので、変数sはもう使えない // s; }
それがわかると単純型、より正確に言えばCopyトレイトが実装されている型の変数において、Rustクロージャのmove
は変数のコピーであることがわかるだろう。
#[allow(path_statements)] fn main() { let mut v = 0i32; // 単純型においてmoveは「変数のコピー」である。 let mut f = move || { v += 1i32; println!("{v}"); }; // Rustクロージャの実行 f(); // 値がコピーされているので変数が使える v; }
先ほどあえて構造体を使っていたのは、単純型を使うと所有権を移動した(単純型においては値をコピーした)ということがわかりにくかったためである*1。次以降は普通に単純型を使っていく。
以上がRustクロージャである。
そもそも「クロージャ」の定義とは?
そもそもクロージャの定義は、形式的には「自由変数を何らかの処理に閉じ込めたもの」である。この「何らかの処理」は、実は関数である必要はない(ただ、ほとんどの場合において関数である)。そのため様々なサイトで見かけるクロージャの和訳「関数閉包」は、あまり最適な訳とは言えない。
イメージとしては、以下のようなイメージである。
ここで重要なのが、閉じ込めるのは「変数のコピー」ではなく、「変数そのもの」であるという点である。つまり、Rustでも使われる「環境をキャプチャする」というのは実は「『変数そのもの』を何らかの処理に閉じる」という意味で、少なくとも「変数の参照」でないとクロージャとは言えないわけである。
ではクロージャが実際にどのように動くものなのか確認していこう。
「クロージャと言えば」で出てくるのがJavaScriptである。このJavaScriptは正真正銘のクロージャをサポートしている。
早速その例を見てみよう。ECMAScript 2015以降をサポートしているブラウザであるとする*2。
/** * クロージャを作る関数。xは自由変数。 */ function createClosure(x) { const xView = () => console.log(`x = ${x}`); // xをログ出力するクロージャ const xUpdate = () => ++x; // xを+1するクロージャ // (1) まずはxをログ出力 xView(); // (2) xを更新してからログ出力 xUpdate(); // xを更新 xView(); // xをログ出力 // (3) xUpdateを戻り値として返す return xUpdate; } // クロージャを生成する const closure = createClosure(0); // (4) クロージャを実行してみる console.log(`closure() = ${closure()}`);
コメントで(1)~(4)までナンバリングしたので順番に処理を追ってみよう。
- (1) まずはxをログ出力
// (1) まずはxをログ出力
xView();
クロージャを生成した際に、まずこの処理が実行される。そしてログには以下のように表示されるはずである。
x = 0
生成の際には引数に0を渡しているのでこれは正しい結果である。
- (2) xを更新してからログ出力
// (2) xを更新してからログ出力 xUpdate(); // xを更新 xView(); // xをログ出力
次に、xUpdate()
でx
を+1
する。その後にもう一度xView()
でログ出力すると、以下のように表示されるはずである。
x = 1
このことから、各クロージャは「変数をコピーしている」のではなく、「変数を参照している」ことがわかる。
- (3) xUpdateを戻り値として返す
// (3) xUpdateを戻り値として返す return xUpdate;
文字通り、xUpdate
を戻り値として返している。つまり、以下の代入では、closure
にxUpdate
が代入されることがわかる。
// クロージャを生成する const closure = createClosure(0);
- (4) クロージャを実行してみる
// (4) クロージャを実行してみる console.log(`closure() = ${closure()}`);
最後にクロージャを実行し、それをログ出力する。すると、以下のように表示される。
closure() = 2
これはとても不思議な結果である。なぜならば、自由変数x
はcreateClosure()
が終了した時点でメモリ解放されているはずだからだ。しかし現にちゃんと表示されている。
これはダングリングポインタ*3によって引き起こした未定義動作*4なのでは決してない。きちんと言語仕様上で定義されている動作である。
つまりJavaScriptの処理系はクロージャを生成する際に自由変数を解放しないように処理してくれるのである。
これこそが正真正銘のクロージャとしての能力である。以上から、クロージャには「『変数そのもの』を閉じ込める」機能が必要であるということがわかったのではないだろうか。
なぜRustのクロージャは「クロージャ」ではないのか?
Rustクロージャでは「環境をキャプチャする」際、move
を付けていなければ自由変数を参照している。ここまでは良い。だが、そういったRustクロージャを戻り値として返すと、ある問題が発生する。早速見て見よう。
/// Rustクロージャの生成。xは自由変数。 /// /// note: Rustクロージャを返すとき戻り値の型をimpl Fn*にする /// ミュータブルなRustクロージャを返す場合はimpl FnMutを返す fn create_closure(mut x: i32) -> impl FnMut() -> i32 { || { x += 1i32; x } }
これはコンパイルエラーになる。
error[E0597]: `x` does not live long enough --> ... | xx | || { | -- value captured here xx | x += 1i32; | ^ borrowed value does not live long enough ... xx | } | - | | | `x` dropped here while still borrowed | borrow later used here For more information about this error, try `rustc --explain E0597`.
なぜかというと、自由変数x
は関数create_closure()
終了時点で解放されるのにも関わらず、戻り値となるRustクロージャで参照して使おうとしているからである。
これを解決させるためには、Rustクロージャの定義にmove
を付ける。
/// Rustクロージャの生成。xは自由変数。 fn create_closure(mut x: i32) -> impl FnMut() -> i32 { move || { x += 1i32; x } } fn main() { let mut closure = create_closure(0i32); // Rustクロージャを生成 // Rustクロージャを実行して標準出力する println!("closure() = {}", closure()); }
しかし先ほども書いたと思うが、move
は所有権の移動またはコピーをすることを示していて、決して「変数そのもの」を閉じ込めているわけではない。つまりこれはクロージャを利用して作ったのではない。
以下のコードを実行してみても、それがわかるだろう。
/// Rustクロージャの生成。xは自由変数。 fn create_closure(mut x: i32) -> impl FnMut() -> i32 { // xを標準出力するRustクロージャ let x_view = move || { println!("x = {x}"); }; // xを+1するRustクロージャ let mut x_update = move || { x += 1i32; x }; // (1) まずは標準出力 x_view(); // (2) xを更新してから標準出力 x_update(); // xを更新 x_view(); // xを標準出力 // (3) x_updateを戻り値として返す x_update } fn main() { let mut closure = create_closure(0i32); // Rustクロージャを生成 // (4) Rustクロージャを実行してみる println!("closure() = {}", closure()); }
Rustのクロージャで疑似的なクロージャを作る
Rustクロージャはクロージャではないということで、簡単にはRustでクロージャが作れないことがわかった。しかし「Rustクロージャでクロージャを作りたい・・・」という人もいるだろうと思うので、おまけとしてRustクロージャで疑似的なクロージャを作ってみる。
まずRustは所有権システムの都合上、2つ以上の変数が同じメモリに対してmut
な操作を行ってはならない。そのため変数とその参照から同時に書き込むと言った操作ができない。
#[allow(unused_assignments)] fn main() { let mut x = 0i32; let ref_x = &mut x; *ref_x += 1i32; x += 1i32; // WARNING: ref_xはもう使えない // *ref_x += 1i32; }
だが幸いなことにRustではmut
変数でなくても変数内部で可変性を持たせることが可能だ。これを内部可変性と言う。
Rustの変数を内部的に可変にするためには、std::cell::Cell
もしくはstd::cell::RefCell
を使う。この2つについての詳細は省くが、以下のように使い分けると良いだろう。
- 型に
Copy
トレイトが実装されている
=>std::cell::Cell
- 型に
Copy
トレイトが実装されていない
=>std::cell::RefCell
では以上の知識をもとに、以下の2つの実装を見ていこう。
(1) ヒープメモリを使わない実装
(2)ではヒープメモリを使うのだが、その際にもう一つ知識が必要となるので、まずはその知識が必要ないヒープメモリを使わない実装から見ていこう。
std::cell::Cell
を使った実装
use std::cell::Cell; /// Rustクロージャの生成。xは自由変数。 fn create_closure(x: Cell<i32>) -> impl FnMut() -> i32 { // xを標準出力するRustクロージャ let x_view = || { println!("x = {}", x.get()); }; // xを+1するRustクロージャ let x_update = || { x.set(x.get() + 1i32); x.get() }; // まずは標準出力 x_view(); // xを更新してから標準出力 x_update(); // xを更新 x_view(); // xを標準出力 // x_updateは戻り値として返せないので、Cellを消費して // 新しくmoveを付けて定義したRustクロージャを返す let mut x = x.into_inner(); move || { x += 1i32; x } } fn main() { let mut closure = create_closure(Cell::new(0i32)); // Rustクロージャを生成 // Rustクロージャを実行してみる println!("closure() = {}", closure()); }
std::cell::RefCell
を使った実装
use std::cell::RefCell; #[derive(Debug, Clone)] struct Struct(i32); /// Rustクロージャの生成。sは自由変数。 fn create_closure(s: RefCell<Struct>) -> impl FnMut() -> Struct { // sを標準出力するRustクロージャ let s_view = || { println!("s = {:?}", s.borrow()); }; // s.0を+1するRustクロージャ let s_update = || { s.borrow_mut().0 += 1i32; <Struct as Clone>::clone(&s.borrow()) }; // まずは標準出力 s_view(); // sを更新してから標準出力 s_update(); // sを更新 s_view(); // sを標準出力 // s_updateは戻り値として返せないので、Cellを消費して // 新しくmoveを付けて定義したRustクロージャを返す let mut s = s.into_inner(); move || { s.0 += 1i32; <Struct as Clone>::clone(&s) } } fn main() { // Rustクロージャを生成 let mut closure = create_closure(RefCell::new(Struct(0i32))); // Rustクロージャを実行してみる println!("closure() = {:?}", closure()); }
この実装では関数内部のRustクロージャでは不変参照しておいて、戻り値として返すときに、新しいRustクロージャを返している。これにより、あたかもクロージャを利用したかのような実装が可能だ。
しかもヒープメモリを使わないため、動作が高速で、組込み環境でも使えるかもしれない*5*6。
(2) ヒープメモリを使った実装
Rustの標準クレートにあるstd::rc::Rc
を使うことで、より「クロージャらしい」実装が可能となる。ただし、std::rc::Rc
は内部でヒープメモリが使われるような実装がされており、基本的に(1)よりは低速となる。
std::cell::Cell
を使った実装
use std::{ cell::Cell, rc::Rc }; /// Rustクロージャの生成。xは自由変数。 /// /// note: 戻り値の型はimpl Fnでも良いが、混乱を避けるためimpl FnMutとする fn create_closure(x: Rc<Cell<i32>>) -> impl FnMut() -> i32 { let x_clone = Rc::clone(&x); // 所有権を複製する // xを標準出力するRustクロージャ let x_view = move || { println!("x = {}", x.get()); }; // xを+1するRustクロージャ let x_update = move || { x_clone.set(x_clone.get() + 1i32); x_clone.get() }; // まずは標準出力 x_view(); // xを更新してから標準出力 x_update(); // xを更新 x_view(); // xを標準出力 // ヒープメモリの場合は自動でメモリを解放するわけではないので // x_updateを戻り値として返せる x_update } fn main() { // Rustクロージャを生成 let mut closure = create_closure(Rc::new(Cell::new(0i32))); // Rustクロージャを実行してみる println!("closure() = {}", closure()); }
std::cell::RefCell
を使った実装
use std::{ cell::RefCell, rc::Rc }; #[derive(Debug, Clone)] struct Struct(i32); /// Rustクロージャの生成。xは自由変数。 fn create_closure(s: Rc<RefCell<Struct>>) -> impl FnMut() -> Struct { let s_clone = Rc::clone(&s); // 所有権を複製する // sを標準出力するRustクロージャ let s_view = move || { println!("s = {:?}", s.borrow()); }; // s.0を+1するRustクロージャ let s_update = move || { s_clone.borrow_mut().0 += 1i32; <Struct as Clone>::clone(&s_clone.borrow()) }; // まずは標準出力 s_view(); // sを更新してから標準出力 s_update(); // sを更新 s_view(); // sを標準出力 // ヒープメモリの場合は自動でメモリを解放するわけではないので // s_updateを戻り値として返せる s_update } fn main() { // Rustクロージャを生成 let mut closure = create_closure(Rc::new(RefCell::new(Struct(0i32)))); // Rustクロージャを実行してみる println!("closure() = {:?}", closure()); }
*1:しかもコードでは"move"と書いているのに実際にやっているのは値のコピーとわかりにくいことこの上ない・・・
*2:見やすさのため。ECMAScript 5以前でも同様の処理を書くことが可能である
*3:メモリが解放されていて、かつ参照できてしまう変数のポインタ
*4:処理系によって動作が変わってしまうことを意味する。何が起こるのかわからないため、基本未定義動作するようなコードは書くべきではない
*5:組込み環境の場合はcore::cell::Cellもしくはcore::cell::RefCellを使うことになる
*6:まぁ組込み環境ならそもそも生ポインタかcore::cell::UnsafeCellを使うことになるのかもしれないが