W.I.S. Laboratory
menu-bar

Rust


RustでJavaScriptの「クロージャ」のようなことをやってみる

Rustでクロージャを作ってみた。
「何を言っているんだこの人は。Rustにはクロージャがあるのだから簡単に作れるだろう」と思うかもしれない。
Rustでクロージャと呼ばれているものは厳密に言うとクロージャではなくて、一般的には「ラムダ式」とか「無名関数」と呼ばれるものだ。

一般的にいう「クロージャ」というのは、「関数閉包」のことだ。(これまた厳密に言うと必ずしも関数である必要はないのだが)
どういうことかというと、「関数(など)の処理に自由変数(などの状態)を閉じ込めたもの」のことを言う。

例えばC言語で、もし関数内に関数が書けるとしたらどうなるだろうか。
内側の関数では自身のローカル変数とその関数の仮引数、そしてグローバル変数にはアクセスできるが、外側の関数で使っているローカル変数にアクセスするための術がない。
内側関数から外側関数のローカル変数にアクセスしたければ、内側関数を呼び出す度に、外側関数のローカル変数(またはそれへのポインタ)をすべて引数で渡すしかないので、コードを書くのも読むのも大変だ。
これを解決するための機能の一つが「クロージャ」だ。(C++やSwiftではこれを「キャプチャ」という機能で解決している)
この機能があると、内側関数があたかも外側関数の中に作ったブロックであるかのように、直感的なコードを書くことができる。

ではその内側関数を、外側関数の外に出してしまうとどうなるか。
例えば、内側関数を外側関数の戻り値として返してみる。
この時点で外側関数は実行が終了し、ローカル変数は消えて無くなっている。
それなのに、内側関数を受け取った場所からも、元外側関数のローカル変数にアクセスできてしまうのだ。
実際はローカル変数そのものではなく、複製された変数なのだが、ではその複製された変数はどこに置かれているのか。
それこそがクロージャだ、ということになる。

クロージャの良いところは、キャプチャよりも直感的なコードが書けるところだ。
キャプチャは、外側関数のローカル変数を値で得るのか参照するのかを明示する必要がある。(どの変数をキャプチャするのかを選択できる言語もある)
例えば値キャプチャだと、内側関数内でキャプチャした変数の値を変更しても、内側関数を抜けた後には元の値に戻ってしまう。(そもそも値が変更できない=イミュータブルになる言語もある)
参照キャプチャを使えば内側関数を抜けた後も値が維持されるので、こういうときは参照キャプチャを使うと便利だ。
しかし今度は参照キャプチャした内側関数を戻り値として返すと、それを受け取った場所から元外側関数のローカル変数にアクセスできなくなってしまう。(C++だとダングリングポインタになってしまう)
なので、内側関数を戻り値とするときは値キャプチャする必要がある。
その時の目的に合わせて、値キャプチャと参照キャプチャを使い分ける必要があるわけだ。
しかしクロージャならそんなことを考えなくても良い。
状況に応じて、より直感的で便利なようにうまく動いてくれる。

そのクロージャが使える言語はいくつかあるが、例えばJavaScriptが有名だ。
JavaScriptの関数はすべてクロージャとなるので、関数が作成されるたびにクロージャが生成される。
A関数から返したB関数、またはA関数から他の関数に渡したB関数が、返した先(渡した先)で生き続けている限り、A関数のローカル変数に対してアクセスできる状態がキープされる。
JavaScriptでのプログラミングは頻繁にクロージャが出てくる上、あまりに直感的に書けるので、特に意識していなくてもクロージャを使っていることが多い。
イベントリスナーにコールバック関数として無名関数を渡した場合など、その無名関数の中から外側関数のローカル変数にアクセスするようなコードを書いていても、イベントが起こって呼び出されたとき、正常にその変数にアクセスできるのは、クロージャ機能があるおかげなのだ。

むちゃくちゃ長くなってしまったが、本題。
そのJavaScriptで関数から返した無名関数のような挙動を、Rustでやってみた。
ただし、Rustの関数はクロージャになれないので、JavaScriptとまったく同じにはできない。
どうするのかというと、Rustのクロージャ(紛らわしいので以降ラムダ式と呼ぶ)のキャプチャ機能を利用する。
キャプチャした変数の値は、(コード上は見えないが)ラムダ式内に格納され、ラムダ式を戻り値とすると、その格納された変数の値ごと関数から返ることになる。
あとはラムダ式内でその値を書き換えれば、ラムダ式が生き続ける限りその値も残ることになる。
まあ、ようするにC++の関数オブジェクトのようなものだ。

まずをラムダ式をミュータブルな状態でBox化する。(キャプチャした変数の値を書き換えられるようにする)
Rustのラムダ式は、Box化すればFnやFnMutといったトレイト型で扱えるようになるので、関数の戻り値にしたり他の関数の引数にしたりすることができるようになる。
ラムダ式を生成する際に、外部変数をmoveしておく。
moveすると所有権が絡む変数は所有権がラムダ式側に移動するが、プリミティブな変数は値がコピーされる。
移動したりコピーしたりした変数の値は、ラムダ式内に格納されている。
このラムダ式を関数から返すと、実際はラムダ式外側の変数はすぐに消えて無くなっているのだが、ラムダ式内ではアクセスできる状態がキープされる。
・・あたかもクロージャのように機能する、という仕掛けだ。


[ 戻る ]
saluteweb