Sehr schnell: Eine Ersetzung ist "referenziell transparent", wenn "Ersetzen von Gleichem zu Gleichem führt", und eine Funktion ist "rein", wenn alle ihre Auswirkungen in ihrem Rückgabewert enthalten sind. Beide können präzisiert werden, aber es ist wichtig zu beachten, dass sie nicht identisch sind und auch nicht das eine das andere impliziert.
Sprechen wir jetzt über Schließungen.
Langweilige (meist reine) "Verschlüsse"
Closures treten auf, weil wir bei der Auswertung eines Lambda-Terms (gebundene) Variablen als Umgebungs-Lookups interpretieren. Wenn wir also einen Lambda-Term als Ergebnis einer Auswertung zurückgeben, haben die darin enthaltenen Variablen die Werte "überschritten", die sie bei ihrer Definition angenommen haben.
In der einfachen Lambda-Rechnung ist dies eine Art Trivialität, und die ganze Vorstellung verschwindet einfach. Um dies zu demonstrieren, ist hier ein relativ leichter Lambda-Kalkül-Interpreter:
-- 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)
Der wichtige Punkt, den Sie beachten müssen, ist, addEnv
wenn Sie der Umgebung einen neuen Namen geben. Diese Funktion wird nur "innerhalb" des interpretierten Abs
Traktionsbegriffs (Lambda-Terms) aufgerufen . Die Umwelt wird „nachgeschlagen“ , wann immer wir eine bewerten Var
Begriff und so diejenigen Var
s Entschlossenheit, unabhängig von der Name
genannten in der Env
die durch die gefangen wurde Abs
Traktion enthält das Var
.
Auch dies ist in einfachen LC-Begriffen langweilig. Dies bedeutet, dass gebundene Variablen für jeden nur Konstanten sind. Sie werden direkt und sofort als die Werte ausgewertet, die sie in der Umgebung als bis zu diesem Zeitpunkt lexikalisch gültig bezeichnen.
Das ist auch (fast) rein. Die einzige Bedeutung eines Begriffs in unserer Lambda-Rechnung wird durch seinen Rückgabewert bestimmt. Die einzige Ausnahme ist die Nebenwirkung der Kündigung, die der Begriff Omega verkörpert:
-- 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")))
Interessante (unreine) Verschlüsse
Vor dem Hintergrund einiger Hintergründe sind die in LC oben beschriebenen Abschlüsse langweilig, da keine Vorstellung davon besteht, mit den Variablen, die wir geschlossen haben, interagieren zu können. Insbesondere neigt das Wort "Closure" dazu, Code wie das folgende Javascript aufzurufen
> function mk_counter() {
var n = 0;
return function incr() {
return n += 1;
}
}
undefined
> var c = mk_counter()
undefined
> c()
1
> c()
2
> c()
3
Dies zeigt, dass wir die n
Variable in der inneren Funktion geschlossen haben incr
und der Aufruf incr
sinnvoll mit dieser Variablen interagiert. mk_counter
ist rein, aber incr
ausgesprochen unrein (und auch nicht referenziell transparent).
Was unterscheidet sich zwischen diesen beiden Fällen?
Begriffe von "Variable"
Wenn wir uns ansehen, was Substitution und Abstraktion im einfachen LC-Sinne bedeuten, stellen wir fest, dass sie eindeutig schlicht sind. Variablen sind buchstäblich nichts anderes als unmittelbare Umgebungssuchvorgänge. Lambda-Abstraktion ist buchstäblich nichts anderes als die Schaffung einer erweiterten Umgebung zur Bewertung des inneren Ausdrucks. Es gibt keinen Platz in diesem Modell für die Art von Verhalten , das wir Säge mit mk_counter
/ incr
weil es keine Variation erlaubt.
Für viele ist dies das Herz dessen, was "Variable" bedeutet - Variation. Semantiker unterscheiden jedoch gerne zwischen der Art der in LC verwendeten Variablen und der Art der in Javascript verwendeten "Variablen". Um dies zu tun, neigen sie dazu, letztere "veränderbare Zelle" oder "Schlitz" zu nennen.
Diese Nomenklatur folgt der langen historischen Verwendung von "Variable" in der Mathematik, wo sie eher "Unbekannt" bedeutete: Der (mathematische) Ausdruck "x + x" lässt nicht zu, dass x
er sich über die Zeit ändert , sondern soll unabhängig von der Bedeutung sein des (einzelnen, konstanten) Wertes x
nimmt.
Wir sagen daher "slot", um die Fähigkeit hervorzuheben, Werte in einen Slot zu setzen und sie herauszunehmen.
Um die Verwirrung noch weiter zu verstärken, sehen diese "Slots" in Javascript genauso aus wie Variablen: Wir schreiben
var x;
zu erstellen und dann, wenn wir schreiben
x;
Es zeigt an, dass wir den aktuell in diesem Slot gespeicherten Wert nachschlagen. Um dies klarer zu machen, neigen reine Sprachen dazu, sich Slots als Namen (mathematisch, Lambda-Kalkül) vorzustellen. In diesem Fall müssen wir ausdrücklich beschriften, wann wir von einem Slot bekommen oder setzen. Eine solche Schreibweise sieht normalerweise so aus
-- 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
Der Vorteil dieser Notation besteht darin, dass wir jetzt fest zwischen mathematischen Variablen und veränderlichen Zeitnischen unterscheiden können. Variablen können Slots als ihre Werte annehmen, aber der bestimmte Slot, der von einer Variablen benannt wird, ist im gesamten Gültigkeitsbereich konstant.
Mit dieser Notation können wir das mk_counter
Beispiel umschreiben (diesmal in einer Haskell-ähnlichen Syntax, wenn auch ausgesprochen un-Haskell-ähnlichen Semantik):
mkCounter =
let x = newSlot
in (\() -> let old = get x
in get (put x (old + 1)))
In diesem Fall verwenden wir Prozeduren, die diesen veränderlichen Slot manipulieren. Um es zu implementieren, müssten wir nicht nur eine konstante Umgebung mit Namen schließen, x
sondern auch eine veränderbare Umgebung, die alle benötigten Slots enthält. Dies ist näher an der allgemeinen Vorstellung von "Schließung", die die Menschen so sehr lieben.
Auch hier mkCounter
ist sehr unrein. Es ist auch sehr referenziell undurchsichtig. Beachten Sie aber , dass die Nebenwirkungen ergeben sich nicht aus dem Namen Fang oder Schließung , sondern die Einnahme der wandelbaren Zelle und den Seiten bewirken Operationen darauf , wie get
und put
.
Letztendlich denke ich, dass dies die endgültige Antwort auf Ihre Frage ist: Die Reinheit wird nicht durch (mathematische) Variablenerfassung beeinflusst, sondern durch nebenwirkende Operationen, die an veränderlichen Slots durchgeführt werden, die von erfassten Variablen benannt werden.
Es ist nur so, dass in Sprachen, die nicht versuchen, LC nahe zu kommen oder Reinheit nicht aufrechtzuerhalten, diese beiden Konzepte so oft miteinander verschmelzen, dass Verwirrung herrscht.