912

誰か説明してくれませんか?それらの背後にある基本的な概念は理解していますが、それらが同じ意味で使用されているのをよく見かけ、混乱します。

ここまで来て、通常の関数とどう違うのでしょうか?

4

15 に答える 15

755

ラムダは単なる匿名関数、つまり名前なしで定義された関数です。Scheme などの一部の言語では、名前付き関数と同等です。実際、関数定義はラムダを変数に内部的にバインドするように書き直されています。Python などの他の言語では、いくつかの (むしろ不必要な) 違いがありますが、それ以外は同じように動作します。

クロージャは、それが定義された環境を閉じる関数です。これは、パラメーター リストにない変数にアクセスできることを意味します。例:

def func(): return h
def anotherfunc(h):
   return func()

-の環境は未定義であるため、エラーが発生しfuncますグローバル環境のみを閉じます。これはうまくいきます:anotherfunchfunc

def anotherfunc(h):
    def func(): return h
    return func()

ここでは、 と python 2.3 以降 (またはこのような数) でfunc定義されているため、クロージャーがほぼ正しくなったとき (突然変異はまだ機能しません)、これはの環境を閉じて、の内部の変数にアクセスできることを意味します。それ。Python 3.1+ では、キーワードを使用するミューテーションも機能します。anotherfunc anotherfuncnonlocal

もう 1 つの重要な点は、 で評価されなくなった場合でも、 の環境funcを閉じ続けることです。このコードも機能します。anotherfuncanotherfunc

def anotherfunc(h):
    def func(): return h
    return func

print anotherfunc(10)()

これは 10 を出力します。

お気づきのとおり、これはlambdaとは何の関係もありません。これらは 2 つの異なる (関連はありますが) 概念です。

于 2008-10-21T03:58:32.583 に答える
476

この StackOverflow の質問への回答でも、ラムダとクロージャに関して多くの混乱があります。特定のプログラミング言語での実践からクロージャーについて学んだ無作為のプログラマーや他の無知なプログラマーに尋ねる代わりに、ソース(すべてが始まった場所) に旅してください。ラムダとクロージャーは、最初の電子コンピューターが存在する前の 30 年代にアロンゾ チャーチによって発明されたラムダ計算に由来するため、これが私が話しているソースです。

ラムダ計算は、世界で最も単純なプログラミング言語です。その中でできることは次のとおりです:►</p>

  • 適用: ある式を別の式に適用することf x(関数呼び出しと
    考えてください。ここで、 は関数であり、その唯一のパラメーターです)fx
  • 抽象化: 式に現れるシンボルをバインドして、このシンボルが単なる「スロット」、値が入力されるのを待っている空白のボックス、いわば「変数」であることをマークします。これは、ギリシャ文字λ(ラムダ)、記号名 (例: )、式の前にxドットを追加することによって行われます。次に、式を 1 つのパラメーターを期待する関数.に変換します。例:式を取り、この式のシンボルがバインドされた変数であることを伝えます。これは、パラメーターとして指定した値に置き換えることができます。 このように定義された関数は無名であることに注意してください
    λx.x+2x+2x
    – 名前がないので、まだ参照することはできませんが、次のように待機しているパラメーターを指定することで、すぐに呼び出すことができます (アプリケーションを覚えていますか?) (λx.x+2) 7。次に、式 (この場合はリテラル値)が、適用されたラムダの部分式のように7置換されるため、 が得られます。これは、一般的な算術規則によってに還元されます。xx+27+29

これで、謎の 1 つが解決しました
。lambdaは、上記の例の無名関数λx.x+2です。


異なるプログラミング言語では、機能的抽象化 (ラムダ) の構文が異なる場合があります。たとえば、JavaScript では次のようになります。

function(x) { return x+2; }

次のようなパラメーターにすぐに適用できます。

(function(x) { return x+2; })(7)

または、この無名関数 (ラムダ) を変数に格納できます。

var f = function(x) { return x+2; }

これにより効果的に名前が付けられf、後で参照して複数回呼び出すことができます。たとえば、次のようになります。

alert(  f(7) + f(10)  );   // should print 21 in the message box

しかし、名前を付ける必要はありませんでした。すぐに呼び出すことができます:

alert(  function(x) { return x+2; } (7)  );  // should print 9 in the message box

LISP では、ラムダは次のように作成されます。

(lambda (x) (+ x 2))

そして、そのようなラムダをパラメーターにすぐに適用することで呼び出すことができます:

(  (lambda (x) (+ x 2))  7  )


OK、今度はもう 1 つの謎を解く時が来ました:クロージャとは何かです。そのために、ラムダ式のシンボル(変数) について話しましょう。

私が言ったように、ラムダ抽象化が行うことは、その部分式でシンボルをバインドすることです。これにより、それは代入可能なパラメーターになります。このようなシンボルはboundと呼ばれます。しかし、式に他の記号がある場合はどうなるでしょうか? 例: λx.x/y+2. この式では、シンボルxはその前にあるラムダ抽象化によってバインドされてλx.います。しかし、もう 1 つの記号yは束縛されておらず、自由です。それが何であり、どこから来たのかわからないので、それが何を意味し、どのようなを表しているのかわからないため、その意味を理解するまでその式を評価することはできませんy

実際、他の 2 つの記号 と についても同じことが言え2ます+。私たちはこれらの 2 つの記号に非​​常に精通しているため、通常、コンピューターはそれらを認識していないことを忘れており、ライブラリや言語自体などのどこかで定義することで、それらが何を意味するのかをコンピューターに伝える必要があります。

フリーシンボルは、その環境と呼ばれる「周囲のコンテキスト」で、式の外の別の場所で定義されていると考えることができます。環境は、この表現が一部であるより大きな表現である可能性があります (Qui-Gon Jinn が言ったように:「常により大きな魚がいます」;))、またはいくつかのライブラリ、または言語自体 (プリミティブとして)。

これにより、ラムダ式を 2 つのカテゴリに分類できます。

  • CLOSED 式: これらの式で発生するすべてのシンボルは、何らかのラムダ抽象化によってバインドされます。つまり、それらは自己完結型です。周囲のコンテキストを評価する必要はありません。コンビネータとも呼ばれます。
  • OPEN 式: これらの式の一部のシンボルはバインドされていません。つまり、それらのシンボルの一部は自由であり、外部情報を必要とするため、これらのシンボルの定義を提供するまで評価できません。

environmentを指定することで、開いているラムダ式を閉じることができます。これは、これらのすべてのフリー シンボルをいくつかの値 (数値、文字列、無名関数、別名ラムダなど) にバインドすることで定義します。

そして、ここでクロージャーの部分が来ます:ラムダ式
クロージャーは、この式内の自由なシンボルに値を与え、それらをもはや非自由にする、外側のコンテキスト (環境) で定義されたこの特定のシンボルのセットです。これは、「未定義」のフリー シンボルをまだ含んでいるオープンラムダ式を、フリー シンボルがなくなったクローズドラムダ式に変換します。

たとえば、次のラムダ式がある場合: λx.x/y+2、シンボルxはバインドされていますが、シンボルyはフリーです。したがって、式は であり、意味openを言わない限り評価できません(また、フリーであるおよびも同様です)。ただし、次のような環境もあるとします。y+2

{  y: 3,
+: [built-in addition],
2: [built-in number],
q: 42,
w: 5  }

この環境は、ラムダ式 ( y+2) のすべての「未定義」 (フリー) シンボルと、いくつかの追加シンボル ( qw) の定義を提供します。定義する必要があるシンボルは、環境のこのサブセットです。

{  y: 3,
+: [built-in addition],
2: [built-in number]  }

これはまさにラムダ式のクロージャーです:>

つまり、開いているラムダ式を閉じます。これがそもそも 名前の閉鎖の由来であり、これがこのスレッドの非常に多くの人々の答えが完全に正しくない理由です:P


では、なぜ彼らは間違っているのでしょうか。クロージャーはメモリ内のデータ構造や、使用する言語の機能の一部であると多くの人が言うのはなぜですか、それともクロージャーとラムダを混同するのはなぜですか? :P

それは、Sun/Oracle、Microsoft、Google などの企業のマーケトイドのせいです。なぜなら、それが彼らの言語 (Java、C#、Go など) でこれらの構造体と呼ばれるものだからです。彼らはしばしば単なるラムダであるはずのものを「クロージャー」と呼んでいます。あるいは、レキシカルスコープを実装するために使用した特定の手法を「クロージャー」と呼んでいます。つまり、関数が定義時に外側のスコープで定義された変数にアクセスできるという事実です。彼らは、関数がこれらの変数を「囲む」、つまり、外部関数の実行が終了した後に変数が破棄されるのを防ぐために、変数を何らかのデータ構造にキャプチャするとよく言います。しかし、これは事後的な「民間伝承の語源」とマーケティングで作られたものであり、物事をより混乱させるだけです。

そして、彼らの言うことには常に少しの真実があり、それを簡単に嘘として却下することはできないという事実のために、それはさらに悪いです:P 説明しましょう:

ラムダを第一級市民として使用する言語を実装する場合は、周囲のコンテキストで定義されたシンボルを使用できるようにする必要があります (つまり、ラムダで自由変数を使用する)。そして、これらのシンボルは、周囲の関数が戻ったときでもそこにある必要があります。問題は、これらのシンボルが関数のローカル ストレージ (通常はコール スタック上) にバインドされていることです。これは、関数が戻るときにもう存在しません。したがって、ラムダが期待どおりに機能するためには、外部コンテキストがなくなった場合でも、これらすべての自由変数を何らかの方法で外部コンテキストから「キャプチャ」し、後で使用できるように保存する必要があります。つまり、クロージャーを見つける必要がありますラムダ (使用するこれらすべての外部変数) を別の場所に保存します (コピーを作成するか、事前にスタック以外の場所にスペースを準備することにより)。この目標を達成するために実際に使用する方法は、言語の「実装の詳細」です。ここで重要なのはクロージャーです。これは、どこかに保存する必要がある、ラムダの環境からの自由変数のセットです。

クロージャを「クロージャ」自体として実装するために、言語の実装で使用する実際のデータ構造を人々が呼び出し始めるのにそれほど時間はかかりませんでした。通常、構造は次のようになります。

Closure {
   [pointer to the lambda function's machine code],
   [pointer to the lambda function's environment]
}

これらのデータ構造は、パラメーターとして他の関数に渡され、関数から返され、変数に格納されてラムダを表し、それらを囲む環境とそのコンテキストで実行するマシン コードにアクセスできるようにします。しかし、これはクロージャー自体ではなく、クロージャーを実装するための (多くの方法の 1 つ) にすぎません。

上で説明したように、ラムダ式のクロージャーは、ラムダ式に含まれる自由変数に値を与える環境内の定義のサブセットであり、式を効果的に閉じます (まだ評価できない開いているラムダ式を含まれているすべてのシンボルが定義されているため、閉じたラムダ式を評価できます)。

それ以外のものは、これらの概念の本当のルーツを知らないプログラマーと言語ベンダーの単なる「カーゴ カルト」と「ブードゥー マジック」です。

それがあなたの質問に答えることを願っています。ただし、フォローアップの質問がある場合は、コメントでお気軽にお尋ねください。より適切に説明できるように努めます。

于 2016-04-27T01:18:04.737 に答える
181

ほとんどの人が関数について考えるとき、名前付き関数を思い浮かべます。

function foo() { return "This string is returned from the 'foo' function"; }

もちろん、これらは名前で呼ばれます。

foo(); //returns the string above

ラムダ式を使用すると、無名関数を使用できます。

 @foo = lambda() {return "This is returned from a function without a name";}

上記の例では、割り当てられた変数を介してラムダを呼び出すことができます。

foo();

ただし、無名関数を変数に割り当てるよりも便利なのは、それらを高階関数 (つまり、他の関数を受け入れる/返す関数) との間で受け渡しすることです。多くの場合、関数に名前を付ける必要はありません。

function filter(list, predicate) 
 { @filteredList = [];
   for-each (@x in list) if (predicate(x)) filteredList.add(x);
   return filteredList;
 }

//filter for even numbers
filter([0,1,2,3,4,5,6], lambda(x) {return (x mod 2 == 0)}); 

クロージャは名前付き関数または無名関数である可能性がありますが、関数が定義されているスコープ内の変数を「閉じる」場合、そのように認識されます。閉鎖そのもの。名前付きクロージャーは次のとおりです。

@x = 0;

function incrementX() { x = x + 1;}

incrementX(); // x now equals 1

大したことではないように思えますが、これがすべて別の関数にありincrementX、外部関数に渡した場合はどうなるでしょうか?

function foo()
 { @x = 0;

   function incrementX() 
    { x = x + 1;
      return x;
    }

   return incrementX;
 }

@y = foo(); // y = closure of incrementX over foo.x
y(); //returns 1 (y.x == 0 + 1)
y(); //returns 2 (y.x == 1 + 1)

これは、関数型プログラミングでステートフル オブジェクトを取得する方法です。「incrementX」という名前は必要ないため、この場合はラムダを使用できます。

function foo()
 { @x = 0;

   return lambda() 
           { x = x + 1;
             return x;
           };
 }
于 2008-10-21T03:46:55.727 に答える
57

すべてのクロージャがラムダであるとは限らず、すべてのラムダがクロージャであるとは限りません。どちらも関数ですが、必ずしも私たちが慣れ親しんだ方法ではありません。

ラムダは本質的に、関数を宣言する標準的な方法ではなく、インラインで定義される関数です。ラムダは、オブジェクトとして頻繁に渡すことができます。

クロージャーは、本体の外部フィールドを参照することによって、周囲の状態を囲む関数です。囲まれた状態は、クロージャーの呼び出し全体で維持されます。

オブジェクト指向言語では、通常、クロージャはオブジェクトを通じて提供されます。ただし、一部の OO 言語 (C#など) は、状態を囲むオブジェクトを持たない純粋関数型言語(Lisp など)によって提供されるクロージャの定義に近い特別な機能を実装しています。

興味深いのは、C# での Lambda と Closures の導入により、関数型プログラミングが主流の使用法に近づいたことです。

于 2008-10-21T03:29:36.743 に答える
13

プログラミング言語の観点からは、これらはまったく別のものです。

基本的に、チューリング完全言語の場合、抽象化、適用、縮小など、非常に限られた要素のみが必要です。抽象化と適用により、ラムダ式を構築する方法が提供され、リダクションによりラムダ式の意味が決定されます。

Lambda は、計算プロセスを抽象化する方法を提供します。たとえば、2 つの数値の合計を計算するには、2 つのパラメーター x、y を取り、x+y を返すプロセスを抽象化できます。スキームでは、次のように書くことができます

(lambda (x y) (+ x y))

パラメータの名前は変更できますが、完了するタスクは変わりません。ほとんどすべてのプログラミング言語で、ラムダ式に名前を付けることができます。これは名前付き関数です。しかし、大きな違いはありません。概念的には、単なる構文糖衣と見なすことができます。

では、これをどのように実装できるか想像してみてください。ラムダ式をいくつかの式に適用するときはいつでも、例えば

((lambda (x y) (+ x y)) 2 3)

パラメータを評価する式に置き換えるだけです。このモデルはすでに非常に強力です。しかし、このモデルでは、シンボルの値を変更することはできません。たとえば、ステータスの変更を模倣することはできません。したがって、より複雑なモデルが必要です。簡単に言うと、ラムダ式の意味を計算するときはいつでも、シンボルと対応する値のペアを環境 (またはテーブル) に入れます。次に、残り (+ xy) は、テーブル内の対応する記号を検索することによって評価されます。ここで、環境を直接操作するためのプリミティブをいくつか提供すると、ステータスの変化をモデル化できます!

この背景を踏まえて、次の機能を確認してください。

(lambda (x y) (+ x y z))

ラムダ式を評価すると、xy が新しいテーブルにバインドされることがわかっています。しかし、どのように、どこで z を調べることができるでしょうか? 実際、z は自由変数と呼ばれます。z を含む外部環境が必要です。そうしないと、x と y をバインドするだけでは式の意味を判断できません。これを明確にするために、スキームで次のように書くことができます。

((lambda (z) (lambda (x y) (+ x y z))) 1)

したがって、z は外部テーブルで 1 にバインドされます。2 つのパラメーターを受け入れる関数も得られますが、その真の意味は外部環境にも依存します。言い換えれば、外部環境は自由変数で閉じます。set! の助けを借りて、関数をステートフルにすることができます。つまり、数学的な意味での関数ではありません。返されるものは、入力だけでなく z にも依存します。

オブジェクトのメソッドは、ほとんどの場合、オブジェクトの状態に依存します。そのため、「クロージャーは貧乏人のオブジェクトだ」と言う人もいます。しかし、ファースト クラス関数が本当に好きなので、オブジェクトを貧乏人のクロージャーと見なすこともできます。

私はスキームを使用してアイデアを説明します。そのスキームは、真のクロージャーを持つ最も初期の言語の 1 つです。ここにあるすべての資料は、SICP の第 3 章でより適切に提示されています。

要約すると、ラムダとクロージャはまったく異なる概念です。ラムダは関数です。クロージャは、ラムダとラムダを閉じる対応する環境のペアです。

于 2014-03-19T00:11:15.193 に答える
11

概念は上記と同じですが、PHP のバックグラウンドがある場合は、PHP コードを使用してさらに説明します。

$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, function ($v) { return $v > 2; });

function ($v) { return $v > 2; } はラムダ関数の定義です。変数に格納することもできるため、再利用できます。

$max = function ($v) { return $v > 2; };

$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, $max);

では、フィルター処理された配列で許可される最大数を変更したい場合はどうすればよいでしょうか? 別のラムダ関数を記述するか、クロージャーを作成する必要があります (PHP 5.3):

$max_comp = function ($max) {
  return function ($v) use ($max) { return $v > $max; };
};

$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, $max_comp(2));

クロージャは、関数が呼び出されたときにアクセスできる 1 つ以上のバインドされた変数を持つ、独自の環境で評価される関数です。それらは、多くの概念が作用する関数型プログラミングの世界から来ています。クロージャはラムダ関数に似ていますが、クロージャが定義されている外部環境からの変数とやり取りできるという意味でよりスマートです。

PHP クロージャの簡単な例を次に示します。

$string = "Hello World!";
$closure = function() use ($string) { echo $string; };

$closure();

この記事でうまく説明されています。

于 2014-07-31T07:41:33.360 に答える
8

この質問は古く、多くの回答があります。
現在、非公式の閉鎖プロジェクトである Java 8 と公式の Lambda により、疑問が復活します。

Java コンテキストでの答え (ラムダとクロージャーを使用 — 違いは何ですか? ):

「クロージャーは、自由変数のそれぞれを値にバインドする環境とペアになったラムダ式です。Java では、ラムダ式はクロージャーによって実装されるため、2 つの用語はコミュニティで同じ意味で使用されるようになりました。」

于 2014-11-09T17:46:45.397 に答える
1

関数が外部変数を使用して演算を行うかどうかによって異なります。

外部変数- 関数のスコープ外で定義された変数。

  • ラムダ式は、操作を実行するためにパラメーター、内部変数、または定数に依存するため、ステートレスです。

    Function<Integer,Integer> lambda = t -> {
        int n = 2
        return t * n 
    }
    
  • クロージャーは、外部変数 (つまり、関数本体のスコープ外で定義された変数) とパラメーターおよび定数を使用して操作を実行するため、状態を保持します。

    int n = 2
    
    Function<Integer,Integer> closure = t -> {
        return t * n 
    }
    

Java がクロージャーを作成するとき、変数 n を関数と共に保持するため、他の関数に渡されたときやどこでも使用されたときに参照できます。

于 2018-09-24T09:34:44.743 に答える
0

Lambda は、 (必ずしも) 識別子にバインドされていない匿名関数定義です。

「匿名関数は、すべての関数が匿名であるラムダ計算の発明におけるアロンゾ・チャーチの研究に端を発しています」 -ウィキペディア

Closure はラムダ関数の実装です。

「ピーター J. ランディンは、1964 年にクロージャーという用語を、彼の SECD マシンで式を評価するために使用される環境部分と制御部分を持つものとして定義しました」 -ウィキペディア

Lambda と Closure の一般的な説明は、他の回答でカバーされています。

C++ のバックグラウンドを持つユーザーにとって、ラムダ式は C++11 で導入されました。ラムダは、匿名関数と関数オブジェクトを作成するための便利な方法と考えてください。

「ラムダと対応するクロージャーの違いは、クラスとクラスのインスタンスの違いとまったく同じです。クラスはソースコードにのみ存在し、実行時には存在しません。実行時に存在するのは、オブジェクトがクラスに対するものであるように、クロージャはラムダに対するものです. 各ラムダ式によって (コンパイル中に) 一意のクラスが生成され、そのクラス型のオブジェクト、つまりクロージャが作成されるため、これは驚くべきことではありません。 (実行時)」。-スコット・マイヤーズ

C++ では、キャプチャする自由変数を明示的に指定する必要があるため、Lambda と Closure のニュアンスを調べることができます。

以下のサンプルでは、​​Lambda 式に自由な変数がなく、空のキャプチャ リスト ( []) が含まれています。これは本質的に通常の関数であり、厳密な意味でのクロージャーは必要ありません。したがって、関数ポインターの引数として渡すこともできます。

void register_func(void(*f)(int val))   // Works only with an EMPTY capture list
{
    int val = 3;
    f(val);
}
 
int main() 
{
    int env = 5;
    register_func( [](int val){ /* lambda body can access only val variable*/ } );
}

周囲の環境からの自由変数がキャプチャ リスト ( [env]) に導入されるとすぐに、クロージャを生成する必要があります。

    register_func( [env](int val){ /* lambda body can access val and env variables*/ } );

これは通常の関数ではなくクロージャーであるため、コンパイル エラーが発生します。
no suitable conversion function from "lambda []void (int val)->void" to "void (*)(int val)" exists

std::functionエラーは、生成されたクロージャーを含む呼び出し可能なターゲットを受け入れる関数ラッパーで修正できます。

void register_func(std::function<void(int val)> f)

C++ の例を使用した詳細な説明については、Lambda と Closureを参照してください。

于 2021-03-01T16:45:01.433 に答える