クロージャとは — 定義・仕組み・例・歴史と無名関数の違い

クロージャの定義・仕組み・例・歴史と無名関数の違いを実践コードでわかりやすく解説する初心者〜中級者向けガイド。

著者: Leandro Alegsa

コンピュータサイエンスにおけるクロージャは、それ自体が環境を持つ関数のことです。この環境には、少なくとも1つのバインドされた変数(数値などのを持つ名前)が含まれます。クロージャは、関数が定義されたときの変数束縛(スコープ)を保持し、その関数が呼び出される時点が関数定義時とは別のスコープにあっても、束縛された変数にアクセスできます。クロージャの環境は、クロージャが使用されるまで束縛変数をメモリ内に保持します。

定義と重要な用語

  • 自由変数(free variables):関数本体で使われているが、その関数のパラメータでも局所変数でもない変数。クロージャはこれらの自由変数をキャプチャして環境に保持します。
  • 環境(environment):自由変数とそれに対応する値の集合。クロージャはこの環境と関数コードを一緒に保持します。
  • レキシカル(静的)スコープ:クロージャの意味を定義するために一般的に使われるスコープルール。関数の外側の文脈で変数が決まります。動的スコープとは異なります。

仕組み(どのように動くか)

クロージャは「関数コード」と「その関数が参照する外側の変数の束(環境))」をペアで保持します。実装上は、しばしば次のようになります:

  • 関数が生成されるとき、その関数が参照する外側の変数への参照(またはコピー)が関数オブジェクトに結び付けられる。
  • その後、外側のスコープが抜けても、参照されている変数はガベージコレクションの対象にならず、クロージャが存在する限り生き続ける。
  • キャプチャは言語によって「値渡し(コピー)」「参照渡し」「閉包のための特殊なオブジェクトに格納」など実装が異なる。たとえば、C++のラムダは明示的に値や参照でキャプチャを指定できますし、Javaのラムダは「実質的にfinal」な変数をキャプチャします。

簡単な例

以下は理解しやすい例です。まず JavaScript の例:

// JavaScript function makeCounter() {   let count = 0;   return function() { // この無名関数がクロージャ     count += 1;     return count;   }; }  const c = makeCounter(); console.log(c()); // 1 console.log(c()); // 2 

上の例では、makeCounter の内部で定義された無名関数が外側の変数 count を参照しており、これがクロージャです。makeCounter の呼び出しが終わっても count はクロージャによって保持されます。

Python の例(ループ内でのキャプチャに注意):

# Python のよくある落とし穴 funcs = [] for i in range(3):     funcs.append(lambda: i)  # 期待: 0,1,2  だが実際は 2,2,2 になる(全て最後の i を参照しているため) for f in funcs:     print(f())  # 2,2,2  # 解決方法(デフォルト引数で値を固定する) funcs = [] for i in range(3):     funcs.append(lambda i=i: i)  for f in funcs:     print(f())  # 0,1,2 

歴史

Peter J. Landin は 1964 年にこのアイデアにクロージャという名前をつけました。クロージャが普及したのは、1975年以降のプログラミング言語「Schemeです。それ以降に作られた多くのプログラミング言語にもクロージャが搭載されています。

無名関数(匿名関数)との違い

匿名関数(名前のない関数)は、しばしばクロージャと混同されますが、両者は別の概念です。重要な点は次のとおりです:

  • 無名関数は「名前を持たない関数」であり、必ずしも外側の環境をキャプチャするわけではありません。
  • 無名関数が外側の変数をキャプチャしていれば、それはクロージャでもあります。逆に、名前付き関数でも外側の環境をキャプチャしていればクロージャになり得ます(「名前付きクロージャ」)。
  • したがって「匿名関数 = クロージャ」という等式は正しくなく、「クロージャ = 環境を保持する関数(匿名か名前付きかは問わない)」が正確です。

実装上の注意と利用例

  • メモリ管理:クロージャが外側の変数を保持するため、不要になっても参照が残るとメモリ解放が遅れる。言語実装はガベージコレクション等で管理しますが、長期間保持する場合は注意が必要です。
  • パフォーマンス:頻繁にクロージャを生成するとオーバーヘッドが出る場合がある。コンパイラやランタイムが最適化するケースも多い。
  • キャプチャのセマンティクス:参照コピーか値コピーかで振る舞いが異なる。言語仕様を確認すること(例:Java は実質的に final な変数のみ、C++ は capture list による指定)。
  • ユースケース:コールバック、イベントハンドラ、部分適用・カリー化、関数工場(ファクトリ)、イテレータやジェネレータの内部状態保持など。
  • デバッグの難しさ:クロージャは状態を隠蔽するため、意図しない副作用や寿命の問題の原因になり得る。適切な設計とテストが重要です。

まとめ

クロージャは「関数」と「その関数が参照する環境(束縛)」を一緒に保持する仕組みで、レキシカルスコープを前提に強力なプログラミング表現力を与えます。匿名関数と混同されがちですが、クロージャは「環境を保持する関数」であり、名前の有無とは独立した性質です。実装や言語仕様によってキャプチャの振る舞いが変わるため、具体的な言語での動作を理解して使うことが重要です。

クロージャとファーストクラスの関数

には、数字や文字などの他の種類のデータや、より単純なパーツで構成されたデータ構造などがあります。プログラミング言語のルールでは、関数に与えることができる値、関数が返すことができる値、変数名に束縛される値を第一級の値としている。また、他の関数を受け取ったり返したりする関数を高次関数と呼ぶ。関数を一級値とする言語の多くは、高階の関数やクロージャも持っている。

例えば、次のようなScheme関数を見てみましょう。

; 少なくともTHRESHOLD部数が販売された全ての書籍のリストを返します。(define (best-selling-books threshold) (filter (lambda (book) (≧ (book-sales book) threshold)) book-list)

この例では、ラムダ式 (lambda (book) (>= (book-sales book) threshold)) が関数 best-selling-books の一部となっています。関数が実行されると、Schemeはラムダ式の値を作らなければなりません。これはラムダのコードと、ラムダ内の自由変数であるしきい値変数への参照を持つクロージャを作ることで行われます。(自由変数とは、値に束縛されていない名前のことです)。

フィルタ関数は、リストの各ブックに対してクロージャを実行し、返すべきブックを選びます。クロージャ自身がしきい値への参照を持っているので、filterがクロージャを実行するたびに、クロージャはその値を使うことができます。filter関数自体は、全く別のファイルに書くこともできます。

同じ例をECMAScript(JavaScript)で書き直してみました。これはクロージャをサポートするもう一つの人気言語です。

// function bestSellingBooks(threshold) { return bookList. filter( function(book) { return book. sales >= threshold; }.     ); }

ECMAScriptでは、ここでlambda代わりにfunctionという単語を使い、filter関数の代わりにArray.filterメソッドを使っていますが、それ以外は同じことを同じように行っているコードです。

関数は、クロージャを作成してそれを返すことができます。次の例は、関数を返す関数です。

スキームで

; fの導関数を近似する関数を返す ; dxの間隔を使って、適切に小さくする必要がある。 (define (derivative f dx) (lambda (x) (/ (- (f (+ x dx)))(f x)) dx))

ECMAScriptでは

// 適当に小さくしたdxの区間を使って、 // fの微分を近似する関数を返す。 function derivative(f, dx) { return function(x) { return (f(x + dx) - f(x))/ dx; }; }

クロージャ環境では、囲み関数(導関数)が戻った後も、束縛変数fdxが保持されます。クロージャのない言語では、これらの値は囲み関数が戻った後に失われてしまう。クロージャを持つ言語では、どのクロージャでも結合変数を持っている限り、結合変数をメモリに保持しなければならない。

クロージャは、無名関数を使って形成する必要はありません。例えば、Pythonは、無名関数のサポートが限られていますが、クロージャを備えています。例えば、上記のECMAScriptの例をPythonで実装する方法の1つは、次のようになります。

# def derivative(f, dx): def gradient(x): return (f(x + dx) - f(x))/ dx return gradient

この例では、gradientという関数が、変数fdxとともにクロージャを作っています。外側にある derivative という関数は、このクロージャを返します。この場合、無名関数でも問題ありません。

def derivative(f, dx): return lambda x: (f(x + dx) - f(x))/ dx

Pythonでは、ラムダ式が他の(値を返すコード)のみを含み、(効果を持つが値を持たないコード)を含まないため、しばしば名前付き関数を代わりに使用しなければならない。しかし、Schemeのような他の言語では、すべてのコードは値を返しますが、Schemeではすべてが式です。

クロージャの使い方

クロージャーには様々な用途があります。

  • ソフトウェアライブラリの設計者は,重要な関数の引数としてクロージャを渡すことで,ユーザが動作をカスタマイズできるようにすることができます.例えば、値をソートする関数は、ユーザが定義した基準に従ってソートされる値を比較するクロージャ引数を受け入れることができます。
  • クロージャは評価を遅らせる、つまり呼び出されるまで何もしないので、制御構造の定義に使うことができます。例えば、分岐(if/then/else)やループ(whileやfor)など、Smalltalkの標準的な制御構造はすべて、メソッドがクロージャを受け入れるオブジェクトを使って定義されています。ユーザーが独自の制御構造を定義することも簡単です。
  • 同じ環境に閉じた複数の機能を作り、その環境を変えることでプライベートなコミュニケーションを図ることができます(割り当て可能な言語で)。

スキーム内

(define foo #f) (define bar #f) (let ((secret-message "none")) (set! foo (lambda (msg) (set! secret-message msg)) (set! bar (lambda () secret-message))) (display (bar)) ; prints "none" (newline) (foo "meet me by the docks at midnight") (display (bar)) ; prints "meet me by the docks at midnight"

注:字句環境を束ねるデータ構造をクロージャーと呼ぶ人もいますが、通常は関数に限定して呼ばれます。

質問と回答

Q:コンピュータサイエンスにおけるクロージャとは何ですか?


A: クロージャとは、それ自身の環境を持つ関数のことです。

Q: クロージャの環境は何を含んでいますか?


A:クロージャの環境には、少なくとも1つの境界変数が含まれています。

Q:クロージャの名前をつけたのは誰ですか?


A: 1964年、ピーター・J・ランディンがクロージャのアイデアを命名しました。

Q:1975年以降にクロージャを普及させたプログラミング言語は?


A: 1975年以降、Schemeというプログラミング言語がクロージャを普及させました。

Q: 無名関数とクロージャは同じものですか?


A:無名関数はクロージャと間違って呼ばれることがありますが、すべての無名関数がクロージャというわけではありません。

Q: 無名関数がクロージャである理由は何ですか?


A: 無名関数は、少なくとも1つの束縛された変数を持つそれ自身の環境を持っていれば、クロージャです。

Q: 名前付きクロージャは匿名ですか?


A: いいえ、名前付きクロージャは匿名ではありません。


百科事典を検索する
AlegsaOnline.com - 2020 / 2025 - License CC3