Molto rapidamente: una sostituzione è "referenzialmente trasparente" se "la sostituzione di like porta a like" e una funzione è "pura" se tutti i suoi effetti sono contenuti nel suo valore di ritorno. Entrambi possono essere resi precisi, ma è fondamentale notare che non sono identici né implicano nemmeno l'uno.
Ora parliamo di chiusure.
"Chiusure" noiose (per lo più pure)
Le chiusure si verificano perché quando valutiamo un termine lambda interpretiamo le variabili (associate) come ricerche di ambiente. Pertanto, quando restituiamo un termine lambda come risultato di una valutazione, le variabili al suo interno avranno "chiuso" i valori che hanno assunto quando è stato definito.
Nel semplice calcolo lambda questo è un po 'banale e l'intera nozione svanisce. Per dimostrarlo, ecco un interprete di calcolo lambda relativamente leggero:
-- 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)
La parte importante da notare è addEnv
quando aumentiamo l'ambiente con un nuovo nome. Questa funzione viene chiamata solo "dentro" del Abs
termine di trazione interpretato (termine lambda). L 'ambiente viene "cercato" ogni volta che valutiamo un Var
termine e quindi quelli si Var
risolvono in qualunque cosa a Name
cui si fa riferimento nella Env
quale sia stata catturata dalla Abs
trazione contenente il Var
.
Ora, di nuovo, in termini semplici di LC questo è noioso. Significa che le variabili associate sono solo costanti per quanto interessa a chiunque. Vengono valutati direttamente e immediatamente come valori che denotano nell'ambiente in modo lessicale fino a quel momento.
Anche questo è (quasi) puro. L'unico significato di qualsiasi termine nel nostro calcolo lambda è determinato dal suo valore di ritorno. L'unica eccezione è l'effetto collaterale della non terminazione che è incarnato dal termine 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")))
Chiusure (impure) interessanti
Ora, per alcuni sfondi, le chiusure descritte nella LC di cui sopra sono noiose perché non si ha la possibilità di interagire con le variabili su cui abbiamo chiuso. In particolare, la parola "chiusura" tende a invocare il codice come il seguente 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
Ciò dimostra che abbiamo chiuso la n
variabile nella funzione interna incr
e che la chiamata incr
interagisce in modo significativo con quella variabile. mk_counter
è puro, ma incr
è decisamente impuro (e neanche referenzialmente trasparente).
Cosa differisce tra questi due casi?
Nozioni di "variabile"
Se guardiamo al significato di sostituzione e astrazione in senso lato LC, notiamo che sono decisamente chiari. Le variabili non sono altro che ricerche di ambiente immediate. L'astrazione lambda non è letteralmente altro che la creazione di un ambiente aumentato per valutare l'espressione interiore. Non c'è spazio in questo modello per il tipo di comportamento che abbiamo visto con mk_counter
/ incr
perché non sono consentite variazioni.
Per molti questo è il cuore di ciò che significa "variabile": variazione. Tuttavia, ai semantisti piace distinguere tra il tipo di variabile utilizzata in LC e il tipo di "variabile" utilizzata in Javascript. Per fare ciò, tendono a definire quest'ultima una "cella mutabile" o "slot".
Questa nomenclatura segue il lungo uso storico di "variabile" in matematica dove significava qualcosa di più simile a "sconosciuto": l'espressione (matematica) "x + x" non consente x
di variare nel tempo, ma deve avere significato indipendentemente del valore (singolo, costante)x
accetta.
Quindi, diciamo "slot" per enfatizzare la capacità di inserire valori in uno slot e di eliminarli.
Per aggiungere ulteriore confusione, in Javascript questi "slot" hanno lo stesso aspetto delle variabili: scriviamo
var x;
per crearne uno e poi quando lo scriviamo
x;
ci indica che stiamo cercando il valore attualmente memorizzato in quello slot. Per renderlo più chiaro, i linguaggi puri tendono a pensare alle slot come a prendere nomi come nomi (matematici, lambda calculus). In questo caso dobbiamo esplicitamente etichettare quando otteniamo o mettiamo da uno slot. Tale notazione tende ad apparire
-- 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
Il vantaggio di questa notazione è che ora abbiamo una netta distinzione tra variabili matematiche e slot mutabili. Le variabili possono assumere gli slot come valori, ma lo slot particolare indicato da una variabile è costante in tutto il suo ambito.
Usando questa notazione possiamo riscrivere l' mk_counter
esempio (questa volta in una sintassi simile a Haskell, sebbene decisamente semantica non simile a Haskell):
mkCounter =
let x = newSlot
in (\() -> let old = get x
in get (put x (old + 1)))
In questo caso stiamo usando procedure che manipolano questo slot mutabile. Per implementarlo dovremmo chiudere non solo un ambiente costante di nomi comex
ma anche un ambiente mutevole contenente tutti gli slot necessari. Questo è più vicino alla nozione comune di "chiusura" che la gente ama così tanto.
Ancora una volta, mkCounter
è molto impuro. È anche molto referenzialmente opaco. Ma noti che gli effetti collaterali non derivano dalla cattura o dalla chiusura del nome ma invece dalla cattura della cellula mutabile e dalle operazioni di effetto collaterale su di essa come get
e put
.
In definitiva, penso che questa sia la risposta finale alla tua domanda: la purezza non è influenzata dall'acquisizione (matematica) di variabili ma invece da operazioni con effetti collaterali eseguite su slot mutabili denominati da variabili acquisite.
È solo quello in lingue che non tentano di avvicinarsi alla LC o non tentano di mantenere la purezza che questi due concetti sono così spesso confusi che portano alla confusione.