なんか考えてることとか

変な人が主にプログラミング関連で考えていることをまとめる。

Rustのクロージャは厳密にはクロージャではない

前々から思っていたのだが、Rustのクロージャという「名前」には違和感があった。なぜなら、クロージャと言うのは本来ただの関数のことを示すわけではないからだ。

だがクロージャについて調べてみて、改めて「Rustのクロージャは厳密にはクロージャではない」ということがはっきりしたので、今回はRustのクロージャクロージャではないということを説明していこうと思う。

Rustの「クロージャ」とは

まずは簡単にRustの「クロージャ」(以下Rustクロージャ)について説明していこう。Rustクロージャは以下のように書くことで定義できる。

let closure = |arg| arg + 1i32;

Rustクロージャの定義は簡素過ぎて逆にわかりづらいので、一つずつ見ていこう。

  1. まず引数は||の間に挟んで定義する。上の例だと、argが引数である。
  2. 引数を定義した後に式を定義する。上の例だとarg + 1i32がそれである。
    • 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)
}

Rust Playground

注意として、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 Playground

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;
}

Rust Playground

それがわかると単純型、より正確に言えばCopyトレイトが実装されている型の変数において、Rustクロージャmove変数のコピーであることがわかるだろう

#[allow(path_statements)]
fn main() {
    let mut v = 0i32;
    // 単純型においてmoveは「変数のコピー」である。
    let mut f = move || {
        v += 1i32;
        println!("{v}");
    };
    
    // Rustクロージャの実行
    f();
    
    // 値がコピーされているので変数が使える
    v;
}

Rust Playground

先ほどあえて構造体を使っていたのは、単純型を使うと所有権を移動した(単純型においては値をコピーした)ということがわかりにくかったためである*1。次以降は普通に単純型を使っていく。

以上がRustクロージャである。

そもそも「クロージャ」の定義とは?

そもそもクロージャの定義は、形式的には「自由変数を何らかの処理に閉じ込めたもの」である。この「何らかの処理」は、実は関数である必要はない(ただ、ほとんどの場合において関数である)。そのため様々なサイトで見かけるクロージャの和訳「関数閉包」は、あまり最適な訳とは言えない。
イメージとしては、以下のようなイメージである。

f:id:opaupafz2:20220416205803p:plain

ここで重要なのが、閉じ込めるのは「変数のコピー」ではなく、「変数そのもの」であるという点である。つまり、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を戻り値として返している。つまり、以下の代入では、closurexUpdateが代入されることがわかる。

// クロージャを生成する
const closure = createClosure(0);

// (4) クロージャを実行してみる
console.log(`closure() = ${closure()}`);

最後にクロージャを実行し、それをログ出力する。すると、以下のように表示される。

closure() = 2

これはとても不思議な結果である。なぜならば、自由変数xcreateClosure()が終了した時点でメモリ解放されているはずだからだ。しかし現にちゃんと表示されている。
これはダングリングポインタ*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());
}

Rust Playground

しかし先ほども書いたと思うが、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 Playground

以上から、Rustのクロージャは厳密にはクロージャではないことがわかる。

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 Playground

だが幸いなことに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());
}

Rust Playground

  • 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 Playground

この実装では関数内部の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());
}

Rust Playground

  • 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());
}

Rust Playground

*1:しかもコードでは"move"と書いているのに実際にやっているのは値のコピーとわかりにくいことこの上ない・・・

*2:見やすさのため。ECMAScript 5以前でも同様の処理を書くことが可能である

*3:メモリが解放されていて、かつ参照できてしまう変数のポインタ

*4:処理系によって動作が変わってしまうことを意味する。何が起こるのかわからないため、基本未定義動作するようなコードは書くべきではない

*5:組込み環境の場合はcore::cell::Cellもしくはcore::cell::RefCellを使うことになる

*6:まぁ組込み環境ならそもそも生ポインタかcore::cell::UnsafeCellを使うことになるのかもしれないが