カリー化について質問したところ、閉鎖について言及されました。閉鎖とは何ですか?カレーとの関係は?
24 に答える
可変範囲
ローカル変数を宣言すると、その変数にはスコープがあります。通常、ローカル変数は、それらを宣言するブロックまたは関数内にのみ存在します。
function() {
var a = 1;
console.log(a); // works
}
console.log(a); // fails
ローカル変数にアクセスしようとすると、ほとんどの言語は現在のスコープで検索し、次に親スコープを経由してルート スコープに到達します。
var a = 1;
function() {
console.log(a); // works
}
console.log(a); // works
ブロックまたは関数の処理が完了すると、そのローカル変数は不要になり、通常はメモリ不足になります。
これは、通常、物事が機能することを期待する方法です。
クロージャーは永続的なローカル変数のスコープです
クロージャは、コードの実行がそのブロックから移動した後でもローカル変数を保持する永続的なスコープです。クロージャーをサポートする言語 (JavaScript、Swift、Ruby など) では、参照を保持していれば、それらの変数が宣言されたブロックの実行が終了した後でも、スコープ (親スコープを含む) への参照を保持できます。そのブロックまたは関数のどこかに。
スコープ オブジェクトとそのすべてのローカル変数は関数に関連付けられており、その関数が持続する限り持続します。
これにより、関数の移植性が得られます。関数が最初に定義されたときにスコープ内にあった変数は、完全に異なるコンテキストで関数を呼び出したとしても、後で関数を呼び出すときにスコープ内にあると予想できます。
例えば
この点を説明する JavaScript の非常に単純な例を次に示します。
outer = function() {
var a = 1;
var inner = function() {
console.log(a);
}
return inner; // this returns a function
}
var fnc = outer(); // execute outer to get inner
fnc();
ここでは、関数内に関数を定義しました。内側の関数は、 を含むすべての外側の関数のローカル変数にアクセスできますa
。変数a
は内部関数のスコープ内にあります。
通常、関数が終了すると、そのすべてのローカル変数が吹き飛ばされます。ただし、内部関数を返し、それを変数に割り当てて、が終了fnc
した後も持続するようにすると、が定義されたときにスコープ内にあったすべての変数も持続します。変数は閉じられています -- クロージャ内にあります。outer
inner
a
変数a
は に完全にプライベートであることに注意してくださいfnc
。これは、JavaScript などの関数型プログラミング言語でプライベート変数を作成する方法です。
ご想像のとおり、呼び出すfnc()
と の値が出力されますa
。これは「1」です。
クロージャのない言語では、変数a
はガベージ コレクションされ、関数のouter
終了時に破棄されます。fnc を呼び出すと、a
存在しないため、エラーがスローされます。
JavaScript では、a
変数のスコープは関数が最初に宣言されたときに作成され、関数が存在し続ける限り持続するため、変数は持続します。
a
のスコープに属しouter
ます。のスコープにinner
は、 のスコープへの親ポインターがありますouter
。fnc
を指す変数ですinner
。a
持続する限りfnc
持続します。a
閉鎖の範囲内です。
さらに読む(見る)
このコードを見て、いくつかの実用的な使用例を含むYouTube ビデオを作成しました。
例を (JavaScript で) 示します。
function makeCounter () {
var count = 0;
return function () {
count += 1;
return count;
}
}
var x = makeCounter();
x(); returns 1
x(); returns 2
...etc...
この関数 makeCounter が行うことは、呼び出されるたびに 1 ずつカウントアップする x と呼ばれる関数を返すことです。x にパラメーターを提供していないため、何らかの方法でカウントを記憶する必要があります。レキシカルスコープと呼ばれるものに基づいて、それを見つける場所を知っています。値を見つけるために定義された場所を探す必要があります。この「隠された」値は、クロージャーと呼ばれるものです。
これが私のカリー化の例です。
function add (a) {
return function (b) {
return a + b;
}
}
var add3 = add(3);
add3(4); returns 7
パラメータ a (3) を指定して add を呼び出すと、add3 と定義している戻り関数のクロージャにその値が含まれていることがわかります。そうすることで、add3 を呼び出すと、加算を実行する値がどこにあるかがわかります。
カイルの答えはかなり良いです。唯一の追加の明確化は、クロージャーが基本的にラムダ関数が作成された時点でのスタックのスナップショットであることだと思います。次に、関数が再実行されると、スタックは関数を実行する前の状態に復元されます。したがって、カイルが言及しているように、その隠し値 ( count
) は、ラムダ関数の実行時に使用できます。
クロージャは、別の関数の状態を参照できる関数です。たとえば、Python では、これはクロージャ「inner」を使用します。
def outer (a):
b = "variable in outer()"
def inner (c):
print a, b, c
return inner
# Now the return value from outer() can be saved for later
func = outer ("test")
func (1) # prints "test variable in outer() 1
クロージャーの理解を容易にするために、クロージャーが手続き型言語でどのように実装されるかを調べることが役立つ場合があります。この説明は、Scheme でのクロージャの単純化された実装に従います。
まず、名前空間の概念を紹介する必要があります。コマンドを Scheme インタプリタに入力すると、式内のさまざまな記号を評価し、それらの値を取得する必要があります。例:
(define x 3)
(define y 4)
(+ x y) returns 7
define 式は、x のスポットに値 3 を格納し、y のスポットに値 4 を格納します。次に、(+ xy) を呼び出すと、インタープリターは名前空間で値を検索し、操作を実行して 7 を返すことができます。
ただし、Scheme には、シンボルの値を一時的にオーバーライドできる式があります。次に例を示します。
(define x 3)
(define y 4)
(let ((x 5))
(+ x y)) returns 9
x returns 3
let キーワードが行うことは、x の値が 5 である新しい名前空間を導入することです。y が 4 であり、返される合計が 9 であることはまだわかります。また、式が終了すると x になることもわかります。は 3 に戻ります。この意味で、x はローカル値によって一時的にマスクされています。
手続き型言語とオブジェクト指向言語には、同様の概念があります。関数内でグローバル変数と同じ名前の変数を宣言すると、同じ効果が得られます。
これをどのように実装しますか?簡単な方法はリンク リストを使用することです。先頭には新しい値が含まれ、末尾には古い名前空間が含まれます。シンボルを検索する必要がある場合は、頭から始めて尾を下っていきます。
ここで、ひとまず第一級関数の実装にスキップしましょう。多かれ少なかれ、関数は、関数が呼び出されて戻り値で最高潮に達したときに実行する一連の命令です。関数を読み込むとき、これらの命令を舞台裏で保存し、関数が呼び出されたときに実行できます。
(define x 3)
(define (plus-x y)
(+ x y))
(let ((x 5))
(plus-x 4)) returns ?
x を 3 と定義し、plus-x をそのパラメーター y に x の値を加えたものと定義します。最後に、x が新しい x によってマスクされた環境で plus-x を呼び出します。この x の値は 5 です。関数 plus-x の操作 (+ xy) を格納するだけの場合は、コンテキスト内にいるためです。 x が 5 の場合、返される結果は 9 になります。これは動的スコープと呼ばれるものです。
ただし、Scheme、Common Lisp、およびその他の多くの言語には、レキシカル スコープと呼ばれるものがあります。操作 (+ xy) を格納するだけでなく、その特定のポイントで名前空間も格納します。そうすれば、値を調べると、このコンテキストでは x が実際には 3 であることがわかります。これはクロージャです。
(define x 3)
(define (plus-x y)
(+ x y))
(let ((x 5))
(plus-x 4)) returns 7
要約すると、リンクされたリストを使用して、関数定義時に名前空間の状態を保存できます。これにより、囲んでいるスコープから変数にアクセスできるようになり、残りの部分に影響を与えずに変数をローカルにマスクする機能が提供されます。プログラム。
自由変数を含まない関数は純粋関数と呼ばれます。
1 つ以上の自由変数を含む関数はクロージャと呼ばれます。
var pure = function pure(x){
return x
// only own environment is used
}
var foo = "bar"
var closure = function closure(){
return foo
// foo is a free variable from the outer environment
}
Closures がお尻を蹴る理由の実世界の例を次に示します...これは、私の Javascript コードから直接出てきたものです。説明しましょう。
Function.prototype.delay = function(ms /*[, arg...]*/) {
var fn = this,
args = Array.prototype.slice.call(arguments, 1);
return window.setTimeout(function() {
return fn.apply(fn, args);
}, ms);
};
そして、これを使用する方法は次のとおりです。
var startPlayback = function(track) {
Player.play(track);
};
startPlayback(someTrack);
たとえば、このコード スニペットが実行されてから 5 秒後に、再生を遅らせて開始したいとします。それは簡単でdelay
、クロージャーです:
startPlayback.delay(5000, someTrack);
// Keep going, do other things
ms で呼び出すdelay
と5000
、最初のスニペットが実行され、渡された引数がそのクロージャーに格納されます。それから 5 秒後、setTimeout
コールバックが発生すると、クロージャーはこれらの変数を維持するため、元のパラメーターを使用して元の関数を呼び出すことができます。
これは一種のカリー化、つまり関数の装飾です。
クロージャーがなければ、何らかの方法で関数の外部でこれらの変数の状態を維持する必要があり、その結果、関数の外部のコードに論理的にその内部に属する何かが散らばってしまいます。クロージャを使用すると、コードの品質と可読性が大幅に向上します。
通常、変数はスコープ ルールによってバインドされます。ローカル変数は、定義された関数内でのみ機能します。閉鎖は、利便性のために一時的にこのルールを破る方法です。
def n_times(a_thing)
return lambda{|n| a_thing * n}
end
上記のコードでは、ラムダ (無名関数作成者) によって参照されるlambda(|n| a_thing * n}
ため、クロージャです。a_thing
ここで、結果の無名関数を関数変数に入れるとします。
foo = n_times(4)
foo は通常のスコープ規則を破り、内部で 4 を使用し始めます。
foo.call(3)
12 を返します。
つまり、関数ポインターは、プログラム コード ベース内の場所への単なるポインターです (プログラム カウンターなど)。Closure = Function pointer + Stack frame .
.
• クロージャはサブプログラムであり、それが定義された参照環境です。
– プログラム内の任意の場所からサブプログラムを呼び出すことができる場合、参照環境が必要です。
– ネストされたサブプログラムを許可しない静的スコープの言語には、クロージャーは必要ありません
– クロージャが必要になるのは、サブプログラムがネストしたスコープ内の変数にアクセスでき、どこからでも呼び出せる場合だけです
– クロージャーをサポートするために、実装は一部の変数に無制限のエクステントを提供する必要がある場合があります (サブプログラムは、通常はもはや活動していない非ローカル変数にアクセスする可能性があるため)
例
function makeAdder(x) {
return function(y) {return x + y;}
}
var add10 = makeAdder(10);
var add5 = makeAdder(5);
document.write(″add 10 to 20: ″ + add10(20) +
″<br />″);
document.write(″add 5 to 20: ″ + add5(20) +
″<br />″);
Lua.orgから:
関数が別の関数に囲まれて書かれている場合、その関数は、囲んでいる関数からローカル変数に完全にアクセスできます。この機能は字句スコープと呼ばれます。当たり前のように聞こえるかもしれませんが、そうではありません。レキシカル スコープとファースト クラス関数は、プログラミング言語の強力な概念ですが、その概念をサポートする言語はほとんどありません。
これは別の実際の例で、ゲームで人気のあるスクリプト言語である Lua を使用しています。標準入力が使用できないという問題を回避するために、ライブラリ関数の動作方法を少し変更する必要がありました。
local old_dofile = dofile
function dofile( filename )
if filename == nil then
error( 'Can not use default of stdin.' )
end
old_dofile( filename )
end
old_dofile の値は、このコード ブロックがそのスコープを終了すると (ローカルであるため) 消えますが、値はクロージャーで囲まれているため、新しく再定義された dofile 関数はそれにアクセスできます。 「アップバリュー」。
あなたが Java の世界から来ているなら、クロージャーをクラスのメンバー関数と比較することができます。この例を見てください
var f=function(){
var a=7;
var g=function(){
return a;
}
return g;
}
関数g
はクロージャーです:g
閉じますa
。したがってg
、メンバー関数a
と比較したり、クラス フィールドと比較したり、関数f
をクラスと比較したりできます。