「クロージャ」の版間の差分
m 外部リンクの修正 http:// -> https:// (d.hatena.ne.jp) (Botによる編集) |
|||
9行目: | 9行目: | ||
'''例:''' クロージャを使ったカウンタの例を [[Scheme]] で示す。 |
'''例:''' クロージャを使ったカウンタの例を [[Scheme]] で示す。 |
||
< |
<syntaxhighlight lang="scheme"> |
||
(define (new-counter) |
(define (new-counter) |
||
(let ((count 0)) |
(let ((count 0)) |
||
20行目: | 20行目: | ||
(display (c)) ; 2 |
(display (c)) ; 2 |
||
(display (c)) ; 3 |
(display (c)) ; 3 |
||
</syntaxhighlight> |
|||
</source> |
|||
関数 <code>new-counter</code> の中でクロージャが使用されている。<code>c</code> に代入された無名関数は <code>new-counter</code> 内のローカル変数 <code>count</code> を参照している。<code>c</code> を呼び出すたびに <code>count</code> はインクリメントされていく。 |
関数 <code>new-counter</code> の中でクロージャが使用されている。<code>c</code> に代入された無名関数は <code>new-counter</code> 内のローカル変数 <code>count</code> を参照している。<code>c</code> を呼び出すたびに <code>count</code> はインクリメントされていく。 |
||
39行目: | 39行目: | ||
== セマンティクスの違い == |
== セマンティクスの違い == |
||
言語ごとにスコープのセマンティクスが異なるように、クロージャの定義も異なっている。汎用的な定義では、クロージャが捕捉する「環境」とは、あるスコープのすべての変数の束縛の集合である。しかし、この変数の束縛というものの意味も言語ごとに異なっている。[[命令型プログラミング|命令型言語]]では、変数は値を格納するためのメモリ中の位置と束縛される。この束縛は変化せず、束縛された位置にある値が変化する。クロージャは束縛を捕捉しているので、そのような言語での変数への操作は、それがクロージャからであってもなくとも、同一のメモリ領域に対して実行される。例として、ECMAScriptを取り上げると |
言語ごとにスコープのセマンティクスが異なるように、クロージャの定義も異なっている。汎用的な定義では、クロージャが捕捉する「環境」とは、あるスコープのすべての変数の束縛の集合である。しかし、この変数の束縛というものの意味も言語ごとに異なっている。[[命令型プログラミング|命令型言語]]では、変数は値を格納するためのメモリ中の位置と束縛される。この束縛は変化せず、束縛された位置にある値が変化する。クロージャは束縛を捕捉しているので、そのような言語での変数への操作は、それがクロージャからであってもなくとも、同一のメモリ領域に対して実行される。例として、ECMAScriptを取り上げると |
||
< |
<syntaxhighlight lang="javascript"> |
||
var f, g; |
var f, g; |
||
function foo() |
function foo() |
||
52行目: | 52行目: | ||
g(); // "1" |
g(); // "1" |
||
f(); // "2" |
f(); // "2" |
||
</syntaxhighlight> |
|||
</source> |
|||
関数 <code>foo</code> と2つのクロージャがローカル変数 <code>x</code> に束縛された同一のメモリ領域を使用していることに注意。 |
関数 <code>foo</code> と2つのクロージャがローカル変数 <code>x</code> に束縛された同一のメモリ領域を使用していることに注意。 |
||
60行目: | 60行目: | ||
さらに、[[Haskell]]や、[[Common Lisp]]上に作られた[[CLAZY]]パッケージ(言語拡張)など、遅延評価を行う関数型言語では、変数は将来の計算結果に束縛される。例を挙げる。 |
さらに、[[Haskell]]や、[[Common Lisp]]上に作られた[[CLAZY]]パッケージ(言語拡張)など、遅延評価を行う関数型言語では、変数は将来の計算結果に束縛される。例を挙げる。 |
||
< |
<syntaxhighlight lang="haskell"> |
||
foo x y = let r = x / y |
foo x y = let r = x / y |
||
in (\z -> z + r) |
in (\z -> z + r) |
||
f = foo 1 0 |
f = foo 1 0 |
||
main = do putStr (show (f 123)) |
main = do putStr (show (f 123)) |
||
</syntaxhighlight> |
|||
</source> |
|||
<code>r</code> は計算 <code>(x / y)</code> に束縛されており、この場合は[[ゼロ除算|0による除算]]である。しかしながら、クロージャが参照しているのはその値ではなく計算であるので、エラーはクロージャが実行され、実際にその束縛を使おうと試みたときに現れる。 |
<code>r</code> は計算 <code>(x / y)</code> に束縛されており、この場合は[[ゼロ除算|0による除算]]である。しかしながら、クロージャが参照しているのはその値ではなく計算であるので、エラーはクロージャが実行され、実際にその束縛を使おうと試みたときに現れる。 |
||
71行目: | 71行目: | ||
さらなる違いは[[静的スコープ]]である[[制御構造|制御構文]]、[[C言語]]風の言語における <code>[[return文|return]]</code>・<code>[[break文|break]]</code>・<code>[[continue文|continue]]</code> などにおいて現れる。ECMAScriptなどの言語では、これらはクロージャ毎に束縛され、構文上の束縛を隠蔽する。つまり、クロージャ内からの <code>return</code> はクロージャを呼び出したコードに制御を渡す。しかしSmalltalkでは、このような動作はトップレベルでしか起こらず、クロージャに捕捉される。例を示して、この違いを明らかにする。 |
さらなる違いは[[静的スコープ]]である[[制御構造|制御構文]]、[[C言語]]風の言語における <code>[[return文|return]]</code>・<code>[[break文|break]]</code>・<code>[[continue文|continue]]</code> などにおいて現れる。ECMAScriptなどの言語では、これらはクロージャ毎に束縛され、構文上の束縛を隠蔽する。つまり、クロージャ内からの <code>return</code> はクロージャを呼び出したコードに制御を渡す。しかしSmalltalkでは、このような動作はトップレベルでしか起こらず、クロージャに捕捉される。例を示して、この違いを明らかにする。 |
||
< |
<syntaxhighlight lang="smalltalk"> |
||
"Smalltalk" |
"Smalltalk" |
||
foo |
foo |
||
80行目: | 80行目: | ||
bar |
bar |
||
Transcript show: (self foo) "prints 1" |
Transcript show: (self foo) "prints 1" |
||
</syntaxhighlight> |
|||
</source> |
|||
< |
<syntaxhighlight lang="javascript"> |
||
// ECMAScript |
// ECMAScript |
||
function foo() { |
function foo() { |
||
90行目: | 90行目: | ||
} |
} |
||
print(foo()); // prints 0 |
print(foo()); // prints 0 |
||
</syntaxhighlight> |
|||
</source> |
|||
Smalltalkにおける <code>^</code> はECMAScriptにおける <code>return</code> にあたるものだと頭に入れれば、一目見た限りではどちらのコードも同じことをするように見える。違いは、ECMAScriptの例では <code>return</code> はクロージャを抜けるが関数 <code>foo</code> は抜けず、Smalltalkの例では <code>^</code> はクロージャだけではなくメソッド <code>foo</code> をも抜ける、という点である。後者の特徴はより高い表現力をもたらす。Smalltalkの <code>do:</code> は通常のメソッドであり、自然に制御構文が定義できている。一方、ECMAScriptでは <code>return</code> の意味が変わってしまうので、同じ目的には <code>[[foreach文|foreach]]</code> という新しい構文を導入しなければならない。 |
Smalltalkにおける <code>^</code> はECMAScriptにおける <code>return</code> にあたるものだと頭に入れれば、一目見た限りではどちらのコードも同じことをするように見える。違いは、ECMAScriptの例では <code>return</code> はクロージャを抜けるが関数 <code>foo</code> は抜けず、Smalltalkの例では <code>^</code> はクロージャだけではなくメソッド <code>foo</code> をも抜ける、という点である。後者の特徴はより高い表現力をもたらす。Smalltalkの <code>do:</code> は通常のメソッドであり、自然に制御構文が定義できている。一方、ECMAScriptでは <code>return</code> の意味が変わってしまうので、同じ目的には <code>[[foreach文|foreach]]</code> という新しい構文を導入しなければならない。 |
||
96行目: | 96行目: | ||
しかし、スコープを越えて生存する[[継続]]には問題もある。 |
しかし、スコープを越えて生存する[[継続]]には問題もある。 |
||
< |
<syntaxhighlight lang="smalltalk"> |
||
foo |
foo |
||
^[ x: | ^x ] |
^[ x: | ^x ] |
||
103行目: | 103行目: | ||
f := self foo. |
f := self foo. |
||
f value: 123 "error!" |
f value: 123 "error!" |
||
</syntaxhighlight> |
|||
</source> |
|||
上の例でメソッド <code>foo</code> が返すブロックが実行されたとき、<code>foo</code> から値を返そうとする。しかし、<code>foo</code> の呼び出しは既に完了しているので、この操作はエラーとなる。 |
上の例でメソッド <code>foo</code> が返すブロックが実行されたとき、<code>foo</code> から値を返そうとする。しかし、<code>foo</code> の呼び出しは既に完了しているので、この操作はエラーとなる。 |
||
110行目: | 110行目: | ||
Rubyなどの言語では、プログラマが <code>return</code> の振る舞いを選ぶことができる。 |
Rubyなどの言語では、プログラマが <code>return</code> の振る舞いを選ぶことができる。 |
||
< |
<syntaxhighlight lang="ruby"> |
||
def foo |
def foo |
||
f = Proc.new { return "return from foo from inside proc" } |
f = Proc.new { return "return from foo from inside proc" } |
||
125行目: | 125行目: | ||
puts foo # prints "return from foo from inside proc" |
puts foo # prints "return from foo from inside proc" |
||
puts bar # prints "return from bar" |
puts bar # prints "return from bar" |
||
</syntaxhighlight> |
|||
</source> |
|||
この例の <code>Proc.new</code> と <code>lambda</code> はどちらもクロージャを作るための方法である。しかし、それぞれが作ったクロージャの <code>return</code> の振る舞いに関しては、異なるセマンティクスを持っている。 |
この例の <code>Proc.new</code> と <code>lambda</code> はどちらもクロージャを作るための方法である。しかし、それぞれが作ったクロージャの <code>return</code> の振る舞いに関しては、異なるセマンティクスを持っている。 |
||
134行目: | 134行目: | ||
関数はタグ(名前)付きのブロックであり、クロージャ、無名関数などは無名のブロックをもつ。無名ブロックの名前はnilとなる。この仕様の下で、Common Lispには'''return'''および'''return-from'''が用意されている。'''(return 0)'''は'''(return-from nil 0)'''と同値であり、すなわち一番近いブロックから抜け出すことを意味する。従って、どのような状況下でも、ブロックの名前を指定して任意の場所から脱出することができる。以下に例を示す。 |
関数はタグ(名前)付きのブロックであり、クロージャ、無名関数などは無名のブロックをもつ。無名ブロックの名前はnilとなる。この仕様の下で、Common Lispには'''return'''および'''return-from'''が用意されている。'''(return 0)'''は'''(return-from nil 0)'''と同値であり、すなわち一番近いブロックから抜け出すことを意味する。従って、どのような状況下でも、ブロックの名前を指定して任意の場所から脱出することができる。以下に例を示す。 |
||
< |
<syntaxhighlight lang="lisp"> |
||
(defun a (x) |
(defun a (x) |
||
(* 3 |
(* 3 |
||
151行目: | 151行目: | ||
(a 3) |
(a 3) |
||
;--> 3 |
;--> 3 |
||
</syntaxhighlight> |
|||
</source> |
|||
=== C++ === |
=== C++ === |
||
[[C++11]]規格以降でラムダ式が使えるようになった。なお、以下のようにローカル変数のキャプチャの方法を制御することができる。詳細は[[C++11#ラムダ関数とラムダ式|C++11]]を参照。 |
[[C++11]]規格以降でラムダ式が使えるようになった。なお、以下のようにローカル変数のキャプチャの方法を制御することができる。詳細は[[C++11#ラムダ関数とラムダ式|C++11]]を参照。 |
||
< |
<syntaxhighlight lang="cpp"> |
||
#include <iostream> |
#include <iostream> |
||
#include <vector> |
#include <vector> |
||
180行目: | 180行目: | ||
return it != v.end(); |
return it != v.end(); |
||
} |
} |
||
</syntaxhighlight> |
|||
</source> |
|||
== クロージャに類似した言語機能 == |
== クロージャに類似した言語機能 == |
||
=== C === |
=== C === |
||
[[C言語]]では、[[コールバック (情報工学)|コールバック]]をサポートするライブラリ関数の中に、以下のように[[関数へのポインタ]]と付随する任意のデータを指すためのポインタ(例えば汎用ポインタである<code>void*</code>など)という2つの値を受け取るものがある。 |
[[C言語]]では、[[コールバック (情報工学)|コールバック]]をサポートするライブラリ関数の中に、以下のように[[関数へのポインタ]]と付随する任意のデータを指すためのポインタ(例えば汎用ポインタである<code>void*</code>など)という2つの値を受け取るものがある。 |
||
< |
<syntaxhighlight lang="c"> |
||
typedef int CallbackFunctionType(void* userData); |
typedef int CallbackFunctionType(void* userData); |
||
extern int callUserFunction(CallbackFunctionType* callbackFunction, void* userData); |
extern int callUserFunction(CallbackFunctionType* callbackFunction, void* userData); |
||
</syntaxhighlight> |
|||
</source> |
|||
ライブラリ関数<code>callUserFunction</code>がコールバック関数<code>callbackFunction</code>を実行するたび、実行コンテキストとしてデータポインタ<code>userData</code>を使用する。これによってコールバックは状態を管理することができ、登録した任意の情報を参照できる。このイディオムはクロージャと機能面で似ているが、構文面では似ていない。 |
ライブラリ関数<code>callUserFunction</code>がコールバック関数<code>callbackFunction</code>を実行するたび、実行コンテキストとしてデータポインタ<code>userData</code>を使用する。これによってコールバックは状態を管理することができ、登録した任意の情報を参照できる。このイディオムはクロージャと機能面で似ているが、構文面では似ていない。 |
||
198行目: | 198行目: | ||
=== Eiffel === |
=== Eiffel === |
||
[[Eiffel]]にはクロージャを定義するための'''inline agent'''(インラインエージェント)がある。インラインエージェントはルーチンを表すオブジェクトで、次のように利用する。 |
[[Eiffel]]にはクロージャを定義するための'''inline agent'''(インラインエージェント)がある。インラインエージェントはルーチンを表すオブジェクトで、次のように利用する。 |
||
< |
<syntaxhighlight lang="eiffel"> |
||
OK_button.click_event.subscribe( |
OK_button.click_event.subscribe( |
||
agent(x, y: INTEGER) do |
agent(x, y: INTEGER) do |
||
205行目: | 205行目: | ||
end |
end |
||
) |
) |
||
</syntaxhighlight> |
|||
</source> |
|||
<code>subscribe</code> の実引数はインラインエージェントで、2つの引数を持つ手続きである。ユーザがこのボタンをクリックして、<code>click_event</code> タイプのイベントが起こると、マウスの座標を引数としてこの手続きが実行される。 |
<code>subscribe</code> の実引数はインラインエージェントで、2つの引数を持つ手続きである。ユーザがこのボタンをクリックして、<code>click_event</code> タイプのイベントが起こると、マウスの座標を引数としてこの手続きが実行される。 |
||
213行目: | 213行目: | ||
[[Java]] 7 以前では、[[メソッド (計算機科学)|メソッド]]内部に「ローカルクラス」あるいは「匿名クラス」<ref>{{lang-en-short|anonymous class}}。「無名クラス」とも。[[オラクル (企業)|オラクル]]日本語版サイトの表記に準拠し、匿名クラスとした。</ref>を定義することで似たようなことができる。ローカルクラス/匿名クラスからは、そのメソッドの <code>final</code> (リードオンリー)なローカル変数を、ローカルクラス/匿名クラスの[[フィールド (計算機科学)|フィールド]]と名前が衝突しない限り、参照できる。 |
[[Java]] 7 以前では、[[メソッド (計算機科学)|メソッド]]内部に「ローカルクラス」あるいは「匿名クラス」<ref>{{lang-en-short|anonymous class}}。「無名クラス」とも。[[オラクル (企業)|オラクル]]日本語版サイトの表記に準拠し、匿名クラスとした。</ref>を定義することで似たようなことができる。ローカルクラス/匿名クラスからは、そのメソッドの <code>final</code> (リードオンリー)なローカル変数を、ローカルクラス/匿名クラスの[[フィールド (計算機科学)|フィールド]]と名前が衝突しない限り、参照できる。 |
||
< |
<syntaxhighlight lang="java"> |
||
class CalculationWindow extends JFrame { |
class CalculationWindow extends JFrame { |
||
private JButton saveButton; |
private JButton saveButton; |
||
236行目: | 236行目: | ||
} |
} |
||
} |
} |
||
</syntaxhighlight> |
|||
</source> |
|||
<!-- HACK: 言語機能を説明する場合は、できるかぎりコンソールアプリケーションで例示したほうがよい。 --> |
<!-- HACK: 言語機能を説明する場合は、できるかぎりコンソールアプリケーションで例示したほうがよい。 --> |
||
要素が1つの配列を <code>final</code> な参照で保持すれば、クロージャで1つのローカル変数を参照する機能をエミュレートできる。内部クラスはその参照の値そのものを変えることはできないが、参照されている配列の要素の値は変えることができるからである。このテクニックはJavaに限ったものではなく、[[Python]]など似た制限を持つ言語でも有効である。 |
要素が1つの配列を <code>final</code> な参照で保持すれば、クロージャで1つのローカル変数を参照する機能をエミュレートできる。内部クラスはその参照の値そのものを変えることはできないが、参照されている配列の要素の値は変えることができるからである。このテクニックはJavaに限ったものではなく、[[Python]]など似た制限を持つ言語でも有効である。 |
2020年7月5日 (日) 22:48時点における版
クロージャ(クロージャー、英語: 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
関数 new-counter
の中でクロージャが使用されている。c
に代入された無名関数は new-counter
内のローカル変数 count
を参照している。c
を呼び出すたびに count
はインクリメントされていく。
クロージャの用途
クロージャには多くの用途がある。
- ライブラリの設計者は、関数(コールバック関数)を引数として受け取る関数(高階関数)を定義することで、利用者が挙動をカスタマイズできる汎用的なライブラリ関数を提供することができる。その際、クロージャを高階関数の引数として渡すことで、記述の簡素化や高階関数の外側の状態の参照が可能となる。例えばコレクションのソートを行う関数は、比較関数を引数に渡すことで、利用者が定義した基準でソートできるようになるが、クロージャを使うことでさらに自由度の高い比較処理を簡潔に記述することができるようになる。
- クロージャは遅延評価される(呼び出されるまで何も実行しない)ので、制御構造の定義に用いることができる。例として、Smalltalk の分岐 (if-then-else) や繰り返し (while、for) を含むすべての標準制御構造は、クロージャを引数にとるメソッドを持つオブジェクトを利用することで定義されている。同様な方法で利用者は自作の制御構造を簡単に定義できる。
- 遅延評価される引数のように、その値を求めるためのものは揃っているが、まだ値自体は計算されていない、というものを記憶しておくために、追加の引数を持たないクロージャのようなデータ構造を使う。これをサンク(thinkの過去形)という。ALGOL 60の名前渡しの実装において考案された。
クロージャを持つプログラミング言語
Schemeは完全な静的スコープのクロージャを持つ最初の言語として登場した。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; };
g = function() { --x; };
x = 1;
print(f()); // "2"
}
foo();
g(); // "1"
f(); // "2"
関数 foo
と2つのクロージャがローカル変数 x
に束縛された同一のメモリ領域を使用していることに注意。
一方、多くの関数型言語、例えばML、は変数を直接、値に束縛する。この場合、一度束縛された変数の値を変える方法はないので、クロージャ間で状態を共有する必要はない。単に同じ値を使うだけである。
さらに、Haskellや、Common Lisp上に作られたCLAZYパッケージ(言語拡張)など、遅延評価を行う関数型言語では、変数は将来の計算結果に束縛される。例を挙げる。
foo x y = let r = x / y
in (\z -> z + r)
f = foo 1 0
main = do putStr (show (f 123))
r
は計算 (x / y)
に束縛されており、この場合は0による除算である。しかしながら、クロージャが参照しているのはその値ではなく計算であるので、エラーはクロージャが実行され、実際にその束縛を使おうと試みたときに現れる。
さらなる違いは静的スコープである制御構文、C言語風の言語における return
・break
・continue
などにおいて現れる。ECMAScriptなどの言語では、これらはクロージャ毎に束縛され、構文上の束縛を隠蔽する。つまり、クロージャ内からの return
はクロージャを呼び出したコードに制御を渡す。しかしSmalltalkでは、このような動作はトップレベルでしか起こらず、クロージャに捕捉される。例を示して、この違いを明らかにする。
"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における ^
はECMAScriptにおける return
にあたるものだと頭に入れれば、一目見た限りではどちらのコードも同じことをするように見える。違いは、ECMAScriptの例では return
はクロージャを抜けるが関数 foo
は抜けず、Smalltalkの例では ^
はクロージャだけではなくメソッド foo
をも抜ける、という点である。後者の特徴はより高い表現力をもたらす。Smalltalkの do:
は通常のメソッドであり、自然に制御構文が定義できている。一方、ECMAScriptでは return
の意味が変わってしまうので、同じ目的には foreach
という新しい構文を導入しなければならない。
しかし、スコープを越えて生存する継続には問題もある。
foo
^[ x: | ^x ]
bar
| f |
f := self foo.
f value: 123 "error!"
上の例でメソッド foo
が返すブロックが実行されたとき、foo
から値を返そうとする。しかし、foo
の呼び出しは既に完了しているので、この操作はエラーとなる。
Ruby
Rubyなどの言語では、プログラマが return
の振る舞いを選ぶことができる。
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"
この例の Proc.new
と lambda
はどちらもクロージャを作るための方法である。しかし、それぞれが作ったクロージャの return
の振る舞いに関しては、異なるセマンティクスを持っている。
Common Lisp
Common Lispにおいては、すべての関数、クロージャ、無名関数などは最終的にアセンブリ言語に似たtagbodyに展開されると考えてよい。原理的にはすべてのプログラムは機械語へコンパイルまたはインタープリットされるはずなので、他の言語においてもこの発想は自然である。tagbodyは内部でgo命令によってタグにジャンプすることができる。これも、直接的にアセンブリのラベル及びJMP命令と対応させられる。
関数はタグ(名前)付きのブロックであり、クロージャ、無名関数などは無名のブロックをもつ。無名ブロックの名前はnilとなる。この仕様の下で、Common Lispにはreturnおよびreturn-fromが用意されている。(return 0)は(return-from nil 0)と同値であり、すなわち一番近いブロックから抜け出すことを意味する。従って、どのような状況下でも、ブロックの名前を指定して任意の場所から脱出することができる。以下に例を示す。
(defun a (x)
(* 3
(block b
(* 100
(funcall (lambda (y)
(cond
((= 0 (mod y 3)) (return-from a y)) ; 3の倍数にはaから値をそのまま返す
((oddp y) (return-from b (* 2 y))) ; 奇数には二倍してbまで脱出
(otherwise (return y)))) ; どちらでもなければlambdaからyを返す
x)))))
(a 1)
;--> 6
(a 2)
;--> 600
(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();
}
クロージャに類似した言語機能
C
C言語では、コールバックをサポートするライブラリ関数の中に、以下のように関数へのポインタと付随する任意のデータを指すためのポインタ(例えば汎用ポインタであるvoid*
など)という2つの値を受け取るものがある。
typedef int CallbackFunctionType(void* userData);
extern int callUserFunction(CallbackFunctionType* callbackFunction, void* userData);
ライブラリ関数callUserFunction
がコールバック関数callbackFunction
を実行するたび、実行コンテキストとしてデータポインタuserData
を使用する。これによってコールバックは状態を管理することができ、登録した任意の情報を参照できる。このイディオムはクロージャと機能面で似ているが、構文面では似ていない。
C++
C++では、operator()
をオーバーロードしたクラス(あるいは構造体)により、関数オブジェクトを定義できる。これは関数型言語における関数にいくらか似た振る舞いをみせる。C++の関数オブジェクトは非静的メンバー変数により状態を持つこともできる。しかし、一般的なクロージャのように自動的に(暗黙的に)ローカル変数を捕捉(キャプチャ)するようなことはしない。
また、ローカルクラス、すなわち関数内でクラスを定義することも可能だが、C++11よりも前の規格(C++03以前)ではテンプレート型引数として渡すことができなかったり、暗黙的に参照できる外のローカル変数は static 変数のみであり、自由変数のキャプチャを模倣するためには関数オブジェクトの非静的メンバー変数として明示的に保存しておく必要があったりするなど、後述する Java の無名クラス以上に制約条件が多い。C++11以降のラムダ式は、内部的にはコンパイラによる関数オブジェクトの自動生成により実現されている。したがって、自由変数をキャプチャする際には、関数オブジェクトであってもラムダ式であっても、変数寿命に配慮する必要がある。
Eiffel
Eiffelにはクロージャを定義するためのinline agent(インラインエージェント)がある。インラインエージェントはルーチンを表すオブジェクトで、次のように利用する。
OK_button.click_event.subscribe(
agent(x, y: INTEGER) do
country := map.country_at_coordinates(x, y)
country.display
end
)
subscribe
の実引数はインラインエージェントで、2つの引数を持つ手続きである。ユーザがこのボタンをクリックして、click_event
タイプのイベントが起こると、マウスの座標を引数としてこの手続きが実行される。
Eiffelのインラインエージェントの大きな限界は、外側のスコープのローカル変数を参照できないという点である。
Java 7 以前
Java 7 以前では、メソッド内部に「ローカルクラス」あるいは「匿名クラス」[2]を定義することで似たようなことができる。ローカルクラス/匿名クラスからは、そのメソッドの final
(リードオンリー)なローカル変数を、ローカルクラス/匿名クラスのフィールドと名前が衝突しない限り、参照できる。
class CalculationWindow extends JFrame {
private JButton saveButton;
...
public final void calculateInSeparateThread(final URI uri) {
// The expression "new Runnable() { ... }" is an anonymous class.
Runnable runner = new Runnable() {
void run() {
// It can access final local variables:
calculate(uri);
// It can access private fields of the enclosing class:
// Always update the Graphic components into the Swing Thread
SwingUtilities.invokeLater(new Runnable() {
public void run() {
saveButton.setEnabled(true);
}
});
}
};
new Thread(runner).start();
}
}
要素が1つの配列を final
な参照で保持すれば、クロージャで1つのローカル変数を参照する機能をエミュレートできる。内部クラスはその参照の値そのものを変えることはできないが、参照されている配列の要素の値は変えることができるからである。このテクニックはJavaに限ったものではなく、Pythonなど似た制限を持つ言語でも有効である。
Javaに完全なクロージャを追加するという言語拡張が検討されていた[3]。様々な問題により、クロージャを導入せずに、関数型インタフェース[4]を実装するための簡便な表記法(ラムダ式)が Java 8 にて導入された。
実装
クロージャは典型的には関数コードへのポインタ及び関数の作成時の環境の表現(例えば、使用可能な変数とその値の集合など)を含む特別なデータ構造によって実装される。
ある言語処理系の実行時のメモリモデルがすべてのローカル変数を線形なスタックに確保するものであれば、クロージャを完璧に実装するのは容易ではない。それは、以下のような理由による。
- クロージャをつくった関数(エンクロージャ)の呼び出し元に復帰した際に、クロージャが参照するスタック上のローカル変数(レキシカル変数)が解放されてしまう。しかしクロージャにはレキシカル変数がエンクロージャの終了後も存続することが必要である。したがってレキシカル変数は必要がなくなるまで存続するように確保されなければならない。
- クロージャが実行された時に、レキシカル変数のスタック上の位置を知ることは困難である。
第1の問題を解決するために、クロージャを実装するプログラミング言語は大抵、ガベージコレクションを備えている。この場合、クロージャへの参照が全て無効になった時に、レキシカル変数はガベージコレクタに渡される。
第2の問題を解決するためには、デリゲートのように、関数の参照と実行環境の参照をセットで扱える必要がある。しかし、これではC言語のようなネイティブコードの関数の呼び出しとの互換性がなくなる。そのため、実行時にスタックやヒープに、エンクロージャのスタックポインタを埋め込んだ、実際の関数を起動するだけの小さな関数(トランポリン関数)を動的に生成することでも実装できる。しかし、セキュリティの観点から近代的なOSでは標準でスタックやヒープ上のコードの実行を禁止しているのが一般的であり、この制限を一時的・部分的に解除することをサポートしている環境でなければ実現できない。
現代的なScheme処理系は、クロージャに使用される可能性のあるローカル変数は動的に確保し、そうでないものはスタックに確保するなどの最適化を行うものが多い。
脚注
- ^ クロージャ - JavaScript | MDN
- ^ 英: anonymous class。「無名クラス」とも。オラクル日本語版サイトの表記に準拠し、匿名クラスとした。
- ^ Closures (Lambda Expressions) for the Java Programming Language
- ^ 英: functional interface。抽象メソッドを1つだけもつインタフェース。SAM (Single Abstract Method) typeと呼ばれることもある。
参考文献
- Will Clinger. Foundations of Actor Semantics. MIT Mathematics Doctoral Dissertation. June 1981.