👋

クロージャ 整理

2022/10/26に公開

クロージャ 整理

最近Rustを触り始めてて、クロージャを使うときにmoveborrowを意識させられてたら「あれ、そういえば "JavaScriptのアロー関数"とか "Pythonのlambda" とか雰囲気で使ってたけど、クロージャってどういうメモリモデルなんだ...?」となったので真面目に調べてみる。

そもそもクロージャとは

クロージャ(クロージャー、英語: closure)、関数閉包はプログラミング言語における関数オブジェクトの一種。いくつかの言語ではラムダ式や無名関数にて利用可能な機能・概念である。 (Wikipediaより)

ほう...?
ようするに、関数オブジェクトの仲間らしい。具体的な実装としては、ラムダ式や無名関数といった形で提供されていると。

関数オブジェクト

関数オブジェクト(かんすうオブジェクト、英: function object)は、プログラミング言語において、関数(サブルーチンないしプロシージャ)を、オブジェクトとしたものである。手続きオブジェクトとも言う(プロシージャ=手続き)。(Wikipediaより)

ここで言うオブジェクトっていうのはオブジェクト指向のそれとは違って、第一級オブジェクトのそれ。
ものすごくざっくり言うと、データとして、変数に代入したり引数や戻り値として受け渡したりできるような関数を関数オブジェクト。(雑すぎたらごめんなさい)

まとめると?

ラムダ式や無名関数といった形で提供され、データとして取り扱える関数のこと。(雑すぎたらごめんなさい)
ちなみに、クロージャの生成元となった関数のことをエンクロージャと呼ぶらしい。

実装

一般的には「処理本体へのポインタ + エンクロージャの環境を含むデータ構造」で構成されるらしい。

問題点としては、

  1. エンクロージャ自体が呼び出し元に戻ったとき、クロージャから参照するエンクロージャ内の変数(レキシカル変数)が開放されてしまうので、何かしらの形でエンクロージャの終了後もレキシカル変数が生きている状態にしなければならない
  2. クロージャ実行時、レキシカル変数がどこに確保されているのかは知ることができない

1つめに関しては、Rustのようにborrow checkerでコンパイル時に検査するか、ガベージコレクターでクロージャが開放されるタイミングでエンクロージャのレキシカル変数も開放されるようにする。

2つめに関しては、ちょっとよく理解できてない。

いろんなクロージャ

Rust

よくあるthreadの例。

use std::thread;

// そのまま書くとborrowによって変数が参照される
thread::spawn(|| {
    // ほげほげ
});

// moveをつけると所有権がクロージャに移る (エンクロージャ側で参照しようとすると怒られる)
thread::spawn(move || {
    // ふがふが
});

JavaScript

いわずもがなアロー関数は代表例。

function hoge() {
    // ほげほげ

    let fuga = () => {
        // ふがふが
    };
}

Ruby

ブロックで表現。

def hoge
    // ほげほげ

    fuga = Proc.new do
        // ふがふが
    end
end

C++

C++11以降のラムダ式。

void hoge() {
    // ほげほげ
    
    // [=]でレキシカル変数をコピーで取り込み
    auto fuga1 = [=]() {
        // ふがふが1 
    };
    
    // [&]で参照で取り込み (変数の寿命に注意)
    auto fuga2 = [&]() {
        // ふがふが2
    };
    
    // 明示的に変数の指定もできる
    std::string name = "なまえ";
    auto fuga3 = [&name]() {
        // ふがふが3
    };
}

最後に

Rustのコンパイラさんに怒られまくってたけど、怒ってた理由が腑に落ちました。
クロージャを確保したときのメモリの状態も、なんとなくだけどイメージできるようになった気がしました。(気がしているだけかもしれない)

Discussion