Очень быстро: подстановка «прозрачна по ссылкам», если «подстановка подобного приводит к подобию», а функция «чиста», если все ее эффекты содержатся в возвращаемом значении. Оба из них могут быть сделаны точными, но важно отметить, что они не идентичны и даже не подразумевают другого.
Теперь поговорим о замыканиях.
Скучные (в основном чистые) "замыкания"
Замыкания происходят потому, что когда мы оцениваем лямбда-член, мы интерпретируем (связанные) переменные как поиск окружения. Таким образом, когда мы возвращаем лямбда-член в результате оценки, переменные внутри него будут «закрывать» значения, которые они приняли, когда он был определен.
В простом лямбда-исчислении это тривиально, и само понятие просто исчезает. Чтобы продемонстрировать это, вот относительно легкий интерпретатор лямбда-исчисления:
-- 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
.
Теперь, опять же, в простых терминах ЛНР, это скучно. Это означает, что связанные переменные являются просто константами, насколько это кого-либо волнует. Они оцениваются непосредственно и сразу как значения, которые они обозначают в среде, как лексически ограниченные до этой точки.
Это также (почти) чисто. Единственное значение любого термина в нашем лямбда-исчислении определяется его возвращаемым значением. Единственным исключением является побочный эффект не прекращения, который воплощен в омега-терминах:
-- 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
явно нечисто (и референтно не прозрачно).
Чем отличаются эти два случая?
Понятия "переменная"
Если мы посмотрим, что означают подстановка и абстракция в простом смысле слова LC, мы заметим, что они явно просты. Переменные буквально не более чем непосредственный поиск окружения. Лямбда-абстракция - это буквально не что иное, как создание расширенной среды для оценки внутреннего выражения. В этой модели нет места для того поведения, которое мы видели с mk_counter
/, incr
потому что не допускается никаких изменений.
Для многих это является сердцем того, что означает «переменная» - вариация. Тем не менее, семантикам нравится различать тип переменной, используемой в LC, и тип «переменной», используемой в Javascript. Для этого они, как правило, называют последнюю «изменяемой ячейкой» или «щелью».
Эта номенклатура следует долгому историческому использованию «переменной» в математике, где оно означало нечто более похожее на «неизвестный»: (математическое) выражение «x + x» не допускает 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
.
В конечном счете, я думаю, что это окончательный ответ на ваш вопрос: на чистоту не влияет (математический) захват переменных, а вместо этого побочные эффекты, выполняемые с изменяемыми слотами, названными захваченными переменными.
Только в тех языках, которые не пытаются быть близкими к ЛНР или не пытаются поддерживать чистоту, эти два понятия так часто смешиваются, что приводит к путанице.