誰か説明してくれませんか?それらの背後にある基本的な概念は理解していますが、それらが同じ意味で使用されているのをよく見かけ、混乱します。
ここまで来て、通常の関数とどう違うのでしょうか?
誰か説明してくれませんか?それらの背後にある基本的な概念は理解していますが、それらが同じ意味で使用されているのをよく見かけ、混乱します。
ここまで来て、通常の関数とどう違うのでしょうか?
ラムダは単なる匿名関数、つまり名前なしで定義された関数です。Scheme などの一部の言語では、名前付き関数と同等です。実際、関数定義はラムダを変数に内部的にバインドするように書き直されています。Python などの他の言語では、いくつかの (むしろ不必要な) 違いがありますが、それ以外は同じように動作します。
クロージャは、それが定義された環境を閉じる関数です。これは、パラメーター リストにない変数にアクセスできることを意味します。例:
def func(): return h
def anotherfunc(h):
return func()
-の環境は未定義であるため、エラーが発生しfunc
ます。グローバル環境のみを閉じます。これはうまくいきます:anotherfunc
h
func
def anotherfunc(h):
def func(): return h
return func()
ここでは、 と python 2.3 以降 (またはこのような数) でfunc
定義されているため、クロージャーがほぼ正しくなったとき (突然変異はまだ機能しません)、これはの環境を閉じて、の内部の変数にアクセスできることを意味します。それ。Python 3.1+ では、キーワードを使用するとミューテーションも機能します。anotherfunc
anotherfunc
nonlocal
もう 1 つの重要な点は、 で評価されなくなった場合でも、 の環境func
を閉じ続けることです。このコードも機能します。anotherfunc
anotherfunc
def anotherfunc(h):
def func(): return h
return func
print anotherfunc(10)()
これは 10 を出力します。
お気づきのとおり、これはlambdaとは何の関係もありません。これらは 2 つの異なる (関連はありますが) 概念です。
この StackOverflow の質問への回答でも、ラムダとクロージャに関して多くの混乱があります。特定のプログラミング言語での実践からクロージャーについて学んだ無作為のプログラマーや他の無知なプログラマーに尋ねる代わりに、ソース(すべてが始まった場所) に旅してください。ラムダとクロージャーは、最初の電子コンピューターが存在する前の 30 年代にアロンゾ チャーチによって発明されたラムダ計算に由来するため、これが私が話しているソースです。
ラムダ計算は、世界で最も単純なプログラミング言語です。その中でできることは次のとおりです:►</p>
f x
。(関数呼び出しとf
x
λ
(ラムダ)、記号名 (例: )、式の前にx
ドットを追加することによって行われます。次に、式を 1 つのパラメーターを期待する関数.
に変換します。例:式を取り、この式のシンボルがバインドされた変数であることを伝えます。これは、パラメーターとして指定した値に置き換えることができます。
このように定義された関数は無名であることに注意してくださいλx.x+2
x+2
x
(λx.x+2) 7
。次に、式 (この場合はリテラル値)が、適用されたラムダの部分式のように7
置換されるため、 が得られます。これは、一般的な算術規則によってに還元されます。x
x+2
7+2
9
これで、謎の 1 つが解決しました
。lambdaは、上記の例の無名関数λx.x+2
です。
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 )
私が言ったように、ラムダ抽象化が行うことは、その部分式でシンボルをバインドすることです。これにより、それは代入可能なパラメーターになります。このようなシンボルはboundと呼ばれます。しかし、式に他の記号がある場合はどうなるでしょうか? 例: λx.x/y+2
. この式では、シンボルx
はその前にあるラムダ抽象化によってバインドされてλx.
います。しかし、もう 1 つの記号y
は束縛されておらず、自由です。それが何であり、どこから来たのかわからないので、それが何を意味し、どのような値を表しているのかわからないため、その意味を理解するまでその式を評価することはできませんy
。
実際、他の 2 つの記号 と についても同じことが言え2
ます+
。私たちはこれらの 2 つの記号に非常に精通しているため、通常、コンピューターはそれらを認識していないことを忘れており、ライブラリや言語自体などのどこかで定義することで、それらが何を意味するのかをコンピューターに伝える必要があります。
フリーシンボルは、その環境と呼ばれる「周囲のコンテキスト」で、式の外の別の場所で定義されていると考えることができます。環境は、この表現が一部であるより大きな表現である可能性があります (Qui-Gon Jinn が言ったように:「常により大きな魚がいます」;))、またはいくつかのライブラリ、または言語自体 (プリミティブとして)。
これにより、ラムダ式を 2 つのカテゴリに分類できます。
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
) のすべての「未定義」 (フリー) シンボルと、いくつかの追加シンボル ( q
、w
) の定義を提供します。定義する必要があるシンボルは、環境のこのサブセットです。
{ y: 3,
+: [built-in addition],
2: [built-in number] }
これはまさにラムダ式のクロージャーです:>
つまり、開いているラムダ式を閉じます。これがそもそも 名前の閉鎖の由来であり、これがこのスレッドの非常に多くの人々の答えが完全に正しくない理由です: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 つ) にすぎません。
上で説明したように、ラムダ式のクロージャーは、ラムダ式に含まれる自由変数に値を与える環境内の定義のサブセットであり、式を効果的に閉じます (まだ評価できない開いているラムダ式を含まれているすべてのシンボルが定義されているため、閉じたラムダ式を評価できます)。
それ以外のものは、これらの概念の本当のルーツを知らないプログラマーと言語ベンダーの単なる「カーゴ カルト」と「ブードゥー マジック」です。
それがあなたの質問に答えることを願っています。ただし、フォローアップの質問がある場合は、コメントでお気軽にお尋ねください。より適切に説明できるように努めます。
ほとんどの人が関数について考えるとき、名前付き関数を思い浮かべます。
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;
};
}
すべてのクロージャがラムダであるとは限らず、すべてのラムダがクロージャであるとは限りません。どちらも関数ですが、必ずしも私たちが慣れ親しんだ方法ではありません。
ラムダは本質的に、関数を宣言する標準的な方法ではなく、インラインで定義される関数です。ラムダは、オブジェクトとして頻繁に渡すことができます。
クロージャーは、本体の外部フィールドを参照することによって、周囲の状態を囲む関数です。囲まれた状態は、クロージャーの呼び出し全体で維持されます。
オブジェクト指向言語では、通常、クロージャはオブジェクトを通じて提供されます。ただし、一部の OO 言語 (C#など) は、状態を囲むオブジェクトを持たない純粋関数型言語(Lisp など)によって提供されるクロージャの定義に近い特別な機能を実装しています。
興味深いのは、C# での Lambda と Closures の導入により、関数型プログラミングが主流の使用法に近づいたことです。
プログラミング言語の観点からは、これらはまったく別のものです。
基本的に、チューリング完全言語の場合、抽象化、適用、縮小など、非常に限られた要素のみが必要です。抽象化と適用により、ラムダ式を構築する方法が提供され、リダクションによりラムダ式の意味が決定されます。
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 章でより適切に提示されています。
要約すると、ラムダとクロージャはまったく異なる概念です。ラムダは関数です。クロージャは、ラムダとラムダを閉じる対応する環境のペアです。
概念は上記と同じですが、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();
この質問は古く、多くの回答があります。
現在、非公式の閉鎖プロジェクトである Java 8 と公式の Lambda により、疑問が復活します。
Java コンテキストでの答え (ラムダとクロージャーを使用 — 違いは何ですか? ):
「クロージャーは、自由変数のそれぞれを値にバインドする環境とペアになったラムダ式です。Java では、ラムダ式はクロージャーによって実装されるため、2 つの用語はコミュニティで同じ意味で使用されるようになりました。」
関数が外部変数を使用して演算を行うかどうかによって異なります。
外部変数- 関数のスコープ外で定義された変数。
ラムダ式は、操作を実行するためにパラメーター、内部変数、または定数に依存するため、ステートレスです。
Function<Integer,Integer> lambda = t -> {
int n = 2
return t * n
}
クロージャーは、外部変数 (つまり、関数本体のスコープ外で定義された変数) とパラメーターおよび定数を使用して操作を実行するため、状態を保持します。
int n = 2
Function<Integer,Integer> closure = t -> {
return t * n
}
Java がクロージャーを作成するとき、変数 n を関数と共に保持するため、他の関数に渡されたときやどこでも使用されたときに参照できます。
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を参照してください。