非常に迅速:置換は「likeがlikeにつながる」場合は「参照透過的」であり、戻り値にすべての効果が含まれている場合は関数は「純粋」です。どちらも正確に作成できますが、これらは同一ではなく、一方が他方を暗示していることにも注意することが重要です。
それでは、クロージャについて話しましょう。
退屈な(ほとんど純粋な)「クロージャー」
ラムダ項を評価するときに、(バインドされた)変数を環境ルックアップとして解釈するため、クロージャが発生します。したがって、評価の結果としてラムダ項を返すと、その中の変数は、定義時に取得した値を「クローズ」します。
単純なラムダ計算では、これは一種の些細なことであり、概念全体が消えます。それを示すために、比較的軽量なラムダ計算インタープリターを次に示します。
-- untyped lambda calculus values are functions
data Value = FunVal (Value -> Value)
-- we write expressions where variables take string-based names, but we'll
-- also just assume that nobody ever shadows names to avoid having to do
-- capture-avoiding substitutions
type Name = String
data Expr
= Var Name
| App Expr Expr
| Abs Name Expr
-- We model the environment as function from strings to values,
-- notably ignoring any kind of smooth lookup failures
type Env = Name -> Value
-- The empty environment
env0 :: Env
env0 _ = error "Nope!"
-- Augmenting the environment with a value, "closing over" it!
addEnv :: Name -> Value -> Env -> Env
addEnv nm v e nm' | nm' == nm = v
| otherwise = e nm
-- And finally the interpreter itself
interp :: Env -> Expr -> Value
interp e (Var name) = e name -- variable lookup in the env
interp e (App ef ex) =
let FunVal f = interp e ef
x = interp e ex
in f x -- application to lambda terms
interp e (Abs name expr) =
-- augmentation of a local (lexical) environment
FunVal (\value -> interp (addEnv name value e) expr)
注意すべき重要な部分はaddEnv
、新しい名前で環境を拡張するときです。この関数は、解釈されたAbs
トラクション項(ラムダ項)の「内部」でのみ呼び出されます。環境は、Var
用語を評価するたびに「ルックアップ」されるため、を含むトラクションによってキャプチャされたで参照されているものにVar
解決されます。Name
Env
Abs
Var
繰り返しになりますが、単純なLC用語では、これは退屈です。誰もが気にする限り、バインドされた変数は単なる定数であることを意味します。それらは、その時点までレキシカルにスコープされた環境でそれらが示す値として、直接かつ即座に評価されます。
これも(ほぼ)純粋です。ラムダ計算の用語の唯一の意味は、戻り値によって決まります。唯一の例外は、オメガ用語で具体化される非終了の副作用です。
-- in simple LC syntax:
--
-- (\x -> (x x)) (\x -> (x x))
omega :: Expr
omega = App (Abs "x" (App (Var "x")
(Var "x")))
(Abs "x" (App (Var "x")
(Var "x")))
興味深い(不純な)閉鎖
今、特定の背景については、上記の単純なLCで説明したクロージャーは退屈です。なぜなら、私たちが閉じた変数と対話できるという概念がないからです。特に、「クロージャ」という言葉は、次のJavascriptのようなコードを呼び出す傾向があります
> function mk_counter() {
var n = 0;
return function incr() {
return n += 1;
}
}
undefined
> var c = mk_counter()
undefined
> c()
1
> c()
2
> c()
3
これn
は、内部関数で変数を閉じincr
、呼び出しincr
がその変数と有意に対話することを示しています。mk_counter
純粋ですがincr
、明らかに不純です(また、参照的にも透明ではありません)。
これら2つのインスタンスの違いは何ですか?
「変数」の概念
置換と抽象化が単純なLCの意味で何を意味するかを見ると、それらは明らかに単純であることがわかります。変数は、文字通り、即時の環境検索にすぎません。ラムダ抽象化は、文字通り、内部式を評価するための拡張環境を作成することに他なりません。このモデルには、mk_counter
/で見た種類の振る舞いの余地がありません。これは、incr
許容されるバリエーションがないためです。
多くの人にとって、これは「変数」が意味するもの、つまり変動の中心です。ただし、セマンティクスは、LCで使用される変数の種類とJavascriptで使用される「変数」の種類を区別することを好みます。そうするために、彼らは後者を「可変セル」または「スロット」と呼ぶ傾向があります。
この命名法は、数学における「変数」の長い歴史的な用法に従っており、「未知」のようなものをx
意味します。 (単一の、一定の)値がのx
取ります。
したがって、値をスロットに入れて取り出す能力を強調するために「スロット」と言います。
混乱をさらに深めるために、Javascriptではこれらの「スロット」は変数と同じように見えます。
var x;
作成してから書くとき
x;
これは、そのスロットに現在保存されている値を検索することを示しています。これをより明確にするために、純粋な言語は、スロットを(数学的な、ラムダ計算)の名前と見なす傾向があります。この場合、スロットから取得または挿入するときに明示的にラベルを付ける必要があります。そのような表記は次のように見える傾向があります
-- create a fresh, empty slot and name it `x` in the context of the
-- expression E
let x = newSlot in E
-- look up the value stored in the named slot named `x`, return that value
get x
-- store a new value, `v`, in the slot named `x`, return the slot
put x v
この表記の利点は、数学変数と可変スロットを明確に区別できることです。変数は値としてスロットを取ることができますが、変数によって指定された特定のスロットはそのスコープ全体で一定です。
この表記を使用して、mk_counter
例を書き換えることができます(今回はHaskellのような構文ですが、明らかにHaskellのようなセマンティクスではありません):
mkCounter =
let x = newSlot
in (\() -> let old = get x
in get (put x (old + 1)))
この場合、この可変スロットを操作するプロシージャを使用しています。それを実装するには、名前のような一定の環境だけでなくx
、必要なすべてのスロットを含む可変環境を閉じる必要があります。これは、人々が大好きな「閉鎖」という一般的な概念に近いものです。
繰り返しますが、mkCounter
非常に不純です。また、非常に参照的に不透明です。しかし、予告副作用は、名前のキャプチャまたは閉鎖代わりのキャプチャから生じないことを可変セル等をその上に副作用の操作get
とput
。
最終的に、これはあなたの質問に対する最終的な答えだと思います:純度は(数学的な)変数キャプチャの影響を受けず、キャプチャされた変数によって名前が付けられた可変スロットで実行される副作用操作の影響を受けます。
LCに近づこうとしない言語や、純度を維持しようとしない言語では、これら2つの概念が混同されて混乱を招くことがよくあります。