Bardzo szybko: podstawienie jest „referencyjnie przezroczyste”, jeśli „podstawienie podobnego prowadzi do podobnego”, a funkcja jest „czysta”, jeśli wszystkie jej efekty są zawarte w jej wartości zwracanej. Oba można sprecyzować, ale należy pamiętać, że nie są one identyczne, ani nawet nie implikuje drugiego.
Porozmawiajmy teraz o zamknięciach.
Nudne (głównie czyste) „zamknięcia”
Zamknięcia występują, ponieważ podczas oceny terminu lambda interpretujemy (powiązane) zmienne jako wyszukiwania środowiska. Zatem, gdy zwrócimy termin lambda w wyniku oceny, zmienne w nim zawarte „zamkną” wartości, które przyjęli, gdy zostały zdefiniowane.
W prostym rachunku lambda jest to trochę banalne i całe pojęcie po prostu znika. Aby to wykazać, oto stosunkowo lekki interpreter rachunku 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)
Ważną kwestią, na którą należy zwrócić uwagę, jest fakt, addEnv
że rozszerzamy środowisko o nową nazwę. Ta funkcja jest nazywana tylko „wewnątrz” interpretowanego Abs
terminu trakcji (lambda). Środowisko jest „sprawdzane” za każdym razem, gdy oceniamy dany Var
termin, a zatem Var
rozwiązują one wszystko, Name
o czym mowa w tym, Env
co zostało przechwycone przez Abs
przyczepę zawierającą Var
.
Teraz znowu, w kategoriach LC, jest to nudne. Oznacza to, że zmienne powiązane są tylko stałymi, o ile kogokolwiek to obchodzi. Są one oceniane bezpośrednio i natychmiast, jako wartości, które oznaczają w środowisku, jak do tego momentu, w zakresie leksykalnym.
Jest to również (prawie) czyste. Jedyne znaczenie dowolnego terminu w naszym rachunku lambda zależy od jego wartości zwracanej. Jedynym wyjątkiem jest efekt uboczny braku rozwiązania, który jest ujęty w nazwie 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")))
Interesujące (nieczyste) zamknięcia
Teraz, dla niektórych środowisk, zamknięcia opisane w zwykłym LC powyżej są nudne, ponieważ nie ma pojęcia, że będziemy w stanie oddziaływać na zmienne, które zamknęliśmy. W szczególności słowo „zamknięcie” ma tendencję do wywoływania kodu, takiego jak poniższy 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
To pokazuje, że zamknęliśmy n
zmienną w funkcji wewnętrznej incr
i wywoływanie w sposób incr
znaczący współdziała z tą zmienną. mk_counter
jest czysty, ale incr
zdecydowanie nieczysty (i nie jest również względnie przejrzysty).
Co różni się między tymi dwoma instancjami?
Pojęcia „zmiennej”
Jeśli spojrzymy na znaczenie substytucji i abstrakcji w prostym sensie LC, zauważymy, że są one zdecydowanie jasne. Zmienne są dosłownie niczym więcej niż bezpośrednimi przeglądami środowiska. Abstrakcja Lambda jest dosłownie niczym więcej niż stworzeniem rozszerzonego środowiska do oceny wewnętrznego wyrażenia. W tym modelu nie ma miejsca na zachowanie, które widzieliśmy z mk_counter
/, incr
ponieważ nie jest dozwolona żadna odmiana.
Dla wielu jest to sedno tego, co oznacza „zmienna” - zmienność. Jednak semantycy lubią rozróżniać rodzaj zmiennej używanej w LC od rodzaju „zmiennej” używanej w Javascript. Aby to zrobić, nazywają to „komórką zmienną” lub „gniazdem”.
Ta nomenklatura nawiązuje do długiego historycznego użycia „zmiennej” w matematyce, gdzie oznaczała coś bardziej jak „nieznany”: (matematyczne) wyrażenie „x + x” nie pozwala x
zmieniać się w czasie, zamiast tego ma znaczenie niezależnie wartości (pojedynczej, stałej) x
trwa.
Dlatego mówimy „szczelina”, aby podkreślić możliwość umieszczania wartości w szczelinie i wyjmowania ich.
Aby dodać zamieszanie, w Javascript, te „sloty” wyglądają tak samo jak zmienne: piszemy
var x;
stworzyć taki, a potem, kiedy piszemy
x;
wskazuje, że szukamy wartości aktualnie przechowywanej w tym gnieździe. Aby to wyjaśnić, czyste języki mają tendencję do myślenia o gniazdach jako nazwach (rachunku matematycznym, rachunku lambda). W takim przypadku musimy wyraźnie oznaczyć, kiedy otrzymujemy lub wkładamy z automatu. Taka notacja zwykle wygląda
-- 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
Zaletą tego zapisu jest to, że mamy teraz wyraźne rozróżnienie między zmiennymi matematycznymi a zmiennymi szczelinami. Zmienne mogą przyjmować boksy jako ich wartości, ale konkretny boks nazwany przez zmienną jest stały w całym jego zakresie.
Za pomocą tej notacji możemy przepisać mk_counter
przykład (tym razem w składni podobnej do Haskella, choć zdecydowanie nie podobnej do semantyki podobnej do Haskella):
mkCounter =
let x = newSlot
in (\() -> let old = get x
in get (put x (old + 1)))
W tym przypadku używamy procedur, które manipulują tym zmiennym gniazdem. Aby go zaimplementować, musielibyśmy zamknąć nie tylko stałe środowisko nazw, x
ale także zmienne środowisko zawierające wszystkie potrzebne gniazda. Jest to bliższe powszechnemu pojęciu „zamknięcia”, które ludzie tak bardzo kochają.
Znowu mkCounter
jest bardzo nieczyste. Jest także bardzo referencyjnie nieprzejrzysty. Zauważ jednak, że skutki uboczne nie wynikają z przechwytywania lub zamykania nazwy, ale z przechwytywania modyfikowalnej komórki i działań niepożądanych na niej, takich jak get
i put
.
Ostatecznie myślę, że jest to ostateczna odpowiedź na twoje pytanie: na czystość nie wpływa (matematyczne) przechwytywanie zmiennych, ale zamiast tego działania niepożądane wykonywane na zmiennych gniazdach nazwanych przez przechwycone zmienne.
Tyle tylko, że w językach, które nie próbują być blisko LC ani nie próbują zachować czystości, te dwa pojęcia są tak często splecione, co prowadzi do zamieszania.