Дуже швидко: підміна є "референтно прозорою", якщо "заміна подібних призводить до подібних", а функція "чиста", якщо всі її ефекти містяться у її зворотному значенні. І те і інше можна зробити точними, але важливо відзначити, що вони не тотожні, і навіть одне не означає.
Тепер поговоримо про закриття.
Нудні (здебільшого чисті) "закриття"
Закриття трапляються тому, що, оцінюючи лямбда-термін, ми інтерпретуємо (зв'язані) змінні як пошукові середовища. Таким чином, коли ми повернемо лямбда-термін як результат оцінки, змінні, що знаходяться всередині нього, будуть "закритими" значеннями, прийнятими при визначенні.
У простому лямбдальному обчисленні це щось тривіальне, і все поняття просто зникає. Щоб продемонструвати це, ось відносно легкий перекладач обчислення лямбда:
-- 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
, безумовно, нечистою (а також не відносно прозорою).
Чим відрізняються ці два випадки?
Поняття "змінна"
Якщо ми подивимось на те, що означають підміна і абстракція в простому ЖК-значенні, то помітимо, що вони, безумовно, звичайні. Змінні - це не що інше, як безпосереднє пошуку довкілля. Абстракція лямбда - це не що інше, як створення доповненого середовища для оцінки внутрішнього вираження. У цій моделі немає місця для того, з якою поведінкою ми бачились 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
.
Зрештою, я думаю, що це остаточна відповідь на ваше запитання: на чистоту не впливає (математична) зміна змін, а натомість операції, що виконуються з боку, що виконуються на змінних слотах, названих захопленими змінними.
Тільки те, що в мовах, які не намагаються бути близькими до LC або не намагаються підтримувати чистоту, ці дві концепції так часто плутаються, що призводять до плутанини.