很快:如果“用相似的替换导致相似”替换是“相对透明的”,而如果函数的所有作用都包含在返回值中,则该函数是“纯”的。可以将两者精确化,但必须指出的是,它们既不相同也不暗示彼此。
现在让我们谈谈闭包。
无聊(大多是纯净的)“关闭”
之所以会出现闭包,是因为在评估lambda项时,我们会将变量(绑定)解释为环境查询。因此,当我们返回lambda项作为求值结果时,其中的变量将“覆盖”它们在定义时所取的值。
用普通的lambda演算,这是微不足道的,整个概念就消失了。为了证明这一点,这是一个相对轻量级的lambda演算解释器:
-- 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
s解析为Name
,Env
其中Abs
包含的牵引力捕获了所引用的内容Var
。
现在,再用普通LC术语来说,这很无聊。这意味着就任何人而言,绑定变量只是常量。它们会立即和立即评估为它们在环境中所表示的值(按词法作用域限定)。
这也是(几乎)纯净的。在我们的lambda演算中,任何术语的唯一含义取决于其返回值。唯一的例外是欧米茄(Omega)术语所体现的不终止的副作用:
-- 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中描述的闭包很无聊,因为没有观念能够与我们已经封闭的变量进行交互。特别是,“ closure”一词倾向于调用类似以下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
绝对不纯(并且也不是参照透明的)。
这两个实例之间有什么不同?
“变量”的概念
如果我们看一下普通LC意义上的替换和抽象意味着什么,我们会注意到它们绝对是普通的。实际上,变量无非就是立即环境查找。Lambda抽象实际上只是创建一个增强的环境来评估内部表达式而已。在此模型中,我们对于用mk_counter
/ 看到的行为没有空间,incr
因为不允许有变化。
对于许多人来说,这就是“变量”含义的核心-变异。但是,语义学家喜欢区分LC中使用的变量类型和Javascript中使用的“变量”类型。为此,他们倾向于将后者称为“可变单元”或“插槽”。
这种命名法遵循了“变量”在数学上的悠久历史用法,在该术语中它的含义更像是“未知” :(数学)表达式“ x + x”不允许x
随时间变化,而是意味着无论(单个,常量)值x
的。
因此,我们说“槽”是为了强调将值放入槽并将其取出的能力。
更令人困惑的是,在Javascript中,这些“槽”看起来与变量相同:
var x;
创建一个,然后当我们写
x;
它表示我们正在查找当前存储在该广告位中的值。为了更清楚地说明这一点,纯净的语言倾向于将槽视为将名称视为(数学,lambda演算)名称。在这种情况下,我们必须在从插槽取出或放入插槽时显式标记。这种表示法看起来像
-- 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或不试图保持纯正的语言中,这两个概念经常被混淆,导致混淆。