クロージャクロージャ(クロージャー、英語: closure)、関数閉包はプログラミング言語における関数オブジェクトの一種。いくつかの言語ではラムダ式や無名関数にて利用可能な機能・概念である。引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。関数とそれを評価する環境のペアであるともいえる。この概念は少なくとも1960年代のSECDマシンまで遡ることができる。まれに、関数ではなくとも、環境に紐付けられたデータ構造のことをクロージャと呼ぶ場合もある。クロージャをサポートする言語によるプログラミングでは、単に関数の中に関数を定義することができるだけでなく、その際に、外側の関数(エンクロージャ)で宣言された変数を暗黙的に内側の関数に取り込んで操作することができる。主な利点としてはグローバル変数の削減やコールバック関数記述の簡素化が挙げられる。 典型的にはクロージャは、エンクロージャの内側の関数リテラルや、ネストした関数定義によって必要になる。プログラミング言語により、そのような内側の関数内に出現する自由変数(内側の関数の仮引数でもなく、内側の関数自身のローカル変数でもない変数)の扱いは異なるが、自由変数をレキシカルに(字句的に)参照するのがクロージャである[1]。エンクロージャが実行された際、クロージャが形成される。クロージャは内部の関数のコードとエンクロージャのスコープ内の必要なすべての変数への参照からなる。 クロージャはプログラム内で環境を共有するための仕組みである。レキシカル変数はグローバルな名前空間を占有しないという点でグローバル変数とは異なっている。またオブジェクト指向プログラミングにおけるオブジェクトのインスタンス変数とは、オブジェクトのインスタンスではなく関数の呼び出しに束縛されているという点で異なる。 クロージャは関数型言語では遅延評価やカプセル化のために、また高階関数の引数として広く用いられる。 例: クロージャを使ったカウンタの例を Scheme で示す。 (define (new-counter)
(let ((count 0))
(lambda ()
(set! count (+ count 1))
count)))
(define c (new-counter))
(display (c)) ; 1
(display (c)) ; 2
(display (c)) ; 3
関数 クロージャの用途クロージャには多くの用途がある。
クロージャを持つプログラミング言語本来のラムダ計算は静的スコープだが、1960年のLISP Iは動的スコープという不具合を抱えていて[2]、その後の1960年代のLISPはスコープ解決に色々と問題を抱えていた[3]。1975年にSchemeは完全な静的スコープのクロージャを持つ最初の言語として登場した[4][5]。1984年のCommon Lispはそれを取り入れた。実質的にすべての関数型言語(Scala、Haskell、OCamlなど)とSmalltalkに由来するオブジェクト指向言語は何らかの形でクロージャを持っている。 クラスを使用するオブジェクト指向言語では、完全なクロージャになるにはメソッドの中でクラス定義できることが必要だが、メソッドあるいは関数の中でラムダ式/無名関数が使え、その中から外のローカル変数を読み書きできれば、一般的にはそのプログラミング言語はクロージャを使えるとみなされる。よって、クロージャを持つ言語には、C#(3.0以降)、C++(C++11以降)、ECMAScript(JavaScriptを含む)、Groovy、Java(8以降)、Perl、Python、Ruby、PHP(5.3以降)、Lua、Squirrelなどがある。 セマンティクス[要曖昧さ回避]はそれぞれ大きく異なっているが、多くの現代的な汎用のプログラミング言語は静的スコープとクロージャのいくつかのバリエーションを持っている。 セマンティクスの違い言語ごとにスコープのセマンティクスが異なるように、クロージャの定義も異なっている。汎用的な定義では、クロージャが捕捉する「環境」とは、あるスコープのすべての変数の束縛の集合である。しかし、この変数の束縛というものの意味も言語ごとに異なっている。命令型言語では、変数は値を格納するためのメモリ中の位置と束縛される。この束縛は変化せず、束縛された位置にある値が変化する。クロージャは束縛を捕捉しているので、そのような言語での変数への操作は、それがクロージャからであってもなくとも、同一のメモリ領域に対して実行される。例として、ECMAScriptを取り上げると var f, g;
function foo()
{
var x = 0;
f = function() { x += 1; return x; };
g = function() { x -= 1; return x; };
x = 1;
console.log(f()); // "2"
}
foo();
console.log(g()); // "1"
console.log(f()); // "2"
関数 一方、多くの関数型言語、例えばMLは変数を直接、値に束縛する。この場合、一度束縛された変数の値を変える方法はないので、クロージャ間で状態を共有する必要はない。単に同じ値を使うだけである。 さらに、Haskellなど、遅延評価を行う関数型言語では、変数は将来の計算結果に束縛される。例を挙げる。 foo x y = let r = x / y
in (\z -> z + r)
f = foo 1 0
main = do putStr (show (f 123))
さらなる違いは静的スコープである制御構文、C言語風の言語における "Smalltalk"
foo
| xs |
xs := #(1 2 3 4).
xs do: [:x | ^x].
^0
bar
Transcript show: (self foo) "prints 1"
// ECMAScript
function foo() {
var xs = new Array(1, 2, 3, 4);
xs.forEach(function(x) { return x; });
return 0;
}
print(foo()); // prints 0
Smalltalkにおける しかし、スコープを越えて生存する継続には問題もある。 foo
^[ x: | ^x ]
bar
| f |
f := self foo.
f value: 123 "error!"
上の例でメソッド RubyRubyなどの言語では、プログラマが def foo
f = Proc.new { return "return from foo from inside proc" }
f.call # control leaves foo here
return "return from foo"
end
def bar
f = lambda { return "return from lambda" }
f.call # control does not leave bar here
return "return from bar"
end
puts foo # prints "return from foo from inside proc"
puts bar # prints "return from bar"
この例の Common LispCommon Lispでは、変数束縛を確立するlet、脱出点を確立するblock、Go Toのタグ(ラベル)を確立するtagbodyの三つの要素を基盤とし、これらの三つの組み合わせによって基本的な構文体系が構築されているが、それぞれの構文で確立された要素は、スコープとエクステント(存続期間)という概念によって整理されている。 これら、三つの構文の、変数名、ブロック名、ラベル名は、レキシカルスコープであり、クロージャに閉じ込めることができるが、変数束縛以外は、スコープ外(エクステント外)からアクセスすることはできない。Common Lispでは、これをレキシカルスコープかつ動的エクステントと表現する(変数はレキシカルスコープかつ無限エクステント) blockにより確立された脱出点からは、return-fromによって抜け出す。また、tagbodyによって確立されたタグは、goにより参照される。 (let ((m 3))
(defun a (x)
;; 関数定義は暗黙にblock名として関数名を設定する
(* 3
(block b
(* 100
(funcall
(lambda (y)
(block nil
(tagbody
(cond
((= 0 (mod y m)) (return-from a y)) ;mの倍数にはaから値をそのまま返す(m=3)
((oddp y) (return-from b (* 2 y))) ;奇数には二倍してbから脱出
(T (go exit))) ;どちらでもなければexitへgo toする
;;return-from nilの略記としてreturnが利用可能
exit (return y)))) ;lambda直下のblock nilから脱出
x))))))
(a 1)
;--> 6
(a 2)
;--> 600
(let ((m 6))
;;aの内部で参照するmは定義時のm
(a 3))
;--> 3
C++C++11規格以降でラムダ式が使えるようになった。なお、以下のようにローカル変数のキャプチャの方法を制御することができる。詳細はC++11を参照。 #include <iostream>
#include <vector>
#include <string>
#include <algorithm>
void foo(std::string s) {
int n = 0;
// すべての自由変数をコピーキャプチャ。
auto func1 = [=]() { std::cout << n << ", " << s << std::endl; };
n = 1;
s = "";
func1();
// すべての自由変数を参照キャプチャ。
auto func2 = [&]() { n = -1; s = "hoge"; };
func2();
std::cout << n << ", " << s << std::endl;
}
bool findName(const std::vector<std::string>& v, const std::string& name) {
// 名前を指定して自由変数を参照キャプチャ。
auto it = std::find_if(v.begin(), v.end(), [&name](const std::string& s) { return s == name; });
return it != v.end();
}
クロージャに類似した言語機能CC言語では、コールバックをサポートするライブラリ関数の中に、以下のように関数へのポインタと付随する任意のデータを指すためのポインタ(例えば汎用ポインタである typedef int CallbackFunctionType(void* userData);
extern int callUserFunction(CallbackFunctionType* callbackFunction, void* userData);
ライブラリ関数 C++C++では、 また、ローカルクラス、すなわち関数内でクラスを定義することも可能だが、C++11よりも前の規格(C++03以前)ではテンプレート型引数として渡すことができなかったり、暗黙的に参照できる外のローカル変数は static 変数のみであり、自由変数のキャプチャを模倣するためには関数オブジェクトの非静的メンバー変数として明示的に保存しておく必要があったりするなど、後述する Java の無名クラス以上に制約条件が多い。C++11以降のラムダ式は、内部的にはコンパイラによる関数オブジェクトの自動生成により実現されている。したがって、自由変数をキャプチャする際には、関数オブジェクトであってもラムダ式であっても、変数寿命に配慮する必要がある。 EiffelEiffelにはクロージャを定義するためのinline agent(インラインエージェント)がある。インラインエージェントはルーチンを表すオブジェクトで、次のように利用する。 OK_button.click_event.subscribe(
agent(x, y: INTEGER) do
country := map.country_at_coordinates(x, y)
country.display
end
)
Eiffelのインラインエージェントの大きな限界は、外側のスコープのローカル変数を参照できないという点である。 Java 7 以前Java 7 以前では、メソッド内部に「ローカルクラス」あるいは「匿名クラス」[6]を定義することで似たようなことができる。ローカルクラス/匿名クラスからは、そのメソッドの class CalculationWindow extends JFrame {
private JButton saveButton;
...
public final void calculateInSeparateThread(final URI uri) {
// "new Runnable() { ... }" で匿名クラスを記述する
Runnable runner = new Runnable() {
void run() {
// 匿名クラスの外にあるfinalなローカル変数へアクセスする
calculate(uri);
// 内包するクラスのprivateフィールドにもアクセスできる
// SwingのスレッドからGraphicsコンポーネントを更新する
SwingUtilities.invokeLater(new Runnable() {
public void run() {
saveButton.setEnabled(true);
}
});
}
};
new Thread(runner).start();
}
}
要素が1つの配列を Javaに完全なクロージャを追加するという言語拡張が検討されていた[7]。様々な問題により、クロージャを導入せずに、関数型インタフェース[8]を実装するための簡便な表記法(ラムダ式)が Java 8 にて導入された。 実装クロージャは典型的には関数コードへのポインタ及び関数の作成時の環境の表現(例えば、使用可能な変数とその値の集合など)を含む特別なデータ構造によって実装される。 ある言語処理系の実行時のメモリモデルがすべてのローカル変数を線形なスタックに確保するものであれば、クロージャを完璧に実装するのは容易ではない。それは、以下のような理由による。
第1の問題を解決するために、クロージャを実装するプログラミング言語は大抵、ガベージコレクションを備えている。この場合、クロージャへの参照が全て無効になった時に、レキシカル変数はガベージコレクタに渡される。 第2の問題を解決するためには、デリゲートのように、関数の参照と実行環境の参照をセットで扱える必要がある。しかし、これではC言語のようなネイティブコードの関数の呼び出しとの互換性がなくなる。そのため、実行時にスタックやヒープに、エンクロージャのスタックポインタを埋め込んだ、実際の関数を起動するだけの小さな関数(トランポリン関数)を動的に生成することでも実装できる。しかし、セキュリティの観点から近代的なOSでは標準でスタックやヒープ上のコードの実行を禁止しているのが一般的であり、この制限を一時的・部分的に解除することをサポートしている環境でなければ実現できない。 現代的なScheme処理系は、クロージャに使用される可能性のあるローカル変数は動的に確保し、そうでないものはスタックに確保するなどの最適化を行うものが多い。 やや異なる解法として、Rustではクロージャ内部から外部の変数を使用する場合、参照渡しの他に変数の所有権をクロージャ外部から内部へ渡すことも可能であり、プログラマがクロージャの定義時にどちらを使用するかを選択できる。[9] 後者はデリゲートに似ているが、変数への参照ではなく実体そのものをクロージャが所有するので、これをクロージャのスタック上に確保することができる。これにより、ガベージコレクションを含めクロージャ内部へ渡した変数のための特別な後始末が不要となり、第1および第2の問題をまとめて解決する。一方、クロージャへ渡した変数はその定義以降クロージャ外部では消滅し、予めコピーを作らない限り、参照を含めクロージャ定義以降にて使用するとコンパイルエラーとなる。この解法は、メモリ上での変数の移動が容易である[10]というRustの特徴を利用したものである。 脚注
参考文献
関連項目外部リンク |