Très rapidement: une substitution est "référentiellement transparente" si "la substitution de" conduit à ", et une fonction est" pure "si tous ses effets sont contenus dans sa valeur de retour. Les deux peuvent être précisés, mais il est essentiel de noter qu'ils ne sont pas identiques et que même l'un n'implique pas l'autre.
Parlons maintenant des fermetures.
"Fermetures" ennuyeuses (généralement pures)
Les fermetures se produisent parce que lorsque nous évaluons un terme lambda, nous interprétons les variables (liées) comme des recherches d’environnement. Ainsi, lorsque nous renvoyons un terme lambda à la suite d’une évaluation, les variables qu’il contient auront "fermé" sur les valeurs qu’ils avaient prises lors de sa définition.
Dans le lambda calcul, cela est trivial et toute la notion disparaît. Pour démontrer cela, voici un interprète de calcul lambda relativement léger:
-- 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 partie importante à noter est addEnv
lorsque nous ajoutons un nouveau nom à l'environnement. Cette fonction est appelée uniquement "à l'intérieur" du Abs
terme de traction interprété (terme lambda). L’environnement est "recherché" chaque fois que nous évaluons un Var
terme, ce qui permet de Var
résoudre le problème Name
mentionné dans le Env
qui a été capturé par la Abs
traction contenant le Var
.
Maintenant, encore une fois, en termes simples, LC est ennuyeux. Cela signifie que les variables liées ne sont que des constantes à la portée de tous. Elles sont évaluées directement et immédiatement en tant que valeurs désignées dans l'environnement comme ayant une portée lexicale jusque-là.
C'est aussi (presque) pur. La seule signification d'un terme dans notre lambda calcul est déterminée par sa valeur de retour. La seule exception est l’effet secondaire de la non-résiliation qui est matérialisé par le terme 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")))
Fermetures intéressantes (impures)
Maintenant, dans certains contextes, les fermetures décrites dans LC ci-dessus sont ennuyeuses car il n’ya aucune notion d’interaction avec les variables sur lesquelles nous avons fermé. En particulier, le mot "fermeture" a tendance à invoquer un code comme le code Javascript suivant
> function mk_counter() {
var n = 0;
return function incr() {
return n += 1;
}
}
undefined
> var c = mk_counter()
undefined
> c()
1
> c()
2
> c()
3
Cela montre que nous avons fermé la n
variable dans la fonction interne incr
et que l'appel incr
interagit de manière significative avec cette variable. mk_counter
est pur, mais incr
est décidément impur (et non pas référentiellement transparent non plus).
Qu'est-ce qui diffère entre ces deux instances?
Notions de "variable"
Si nous examinons la signification de substitution et d'abstraction au sens propre du terme LC, nous remarquons qu'elles sont clairement claires. Les variables ne sont littéralement rien de plus que des recherches immédiates sur l'environnement. L'abstraction Lambda n'est littéralement rien de plus que la création d'un environnement augmenté pour évaluer l'expression interne. Ce modèle ne laisse aucune place au type de comportement que nous avons observé avec mk_counter
/ incr
car aucune variation n’est autorisée.
Pour beaucoup, c’est le cœur de ce que signifie "variable": la variation. Cependant, les sémanticistes aiment faire la distinction entre le type de variable utilisé en LC et le type de "variable" utilisé en Javascript. Pour ce faire, ils ont tendance à appeler ce dernier une "cellule mutable" ou un "slot".
Cette nomenclature suit le long usage historique de "variable" en mathématique, où il signifiait plutôt "inconnu": l'expression (mathématique) "x + x" ne permet pas x
de varier dans le temps, elle a plutôt un sens de la valeur (unique, constante) x
prend.
Ainsi, nous disons «slot» pour souligner la capacité de mettre des valeurs dans un slot et de les supprimer.
Pour ajouter à la confusion, en Javascript ces "slots" ressemblent à des variables: nous écrivons
var x;
pour en créer un, puis quand nous écrivons
x;
cela nous indique que nous recherchons la valeur actuellement stockée dans cet emplacement. Pour rendre cela plus clair, les langues pures ont tendance à penser que les machines à sous prennent des noms comme des noms (mathématiques, lambda calcul). Dans ce cas, nous devons explicitement étiqueter quand nous obtenons ou mettons depuis un emplacement. Une telle notation a tendance à ressembler à
-- 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
L'avantage de cette notation est que nous avons maintenant une distinction nette entre les variables mathématiques et les slots mutables. Les variables peuvent prendre des slots comme valeurs, mais le slot particulier nommé par une variable est constant sur toute sa portée.
En utilisant cette notation, nous pouvons réécrire l’ mk_counter
exemple (cette fois-ci dans une syntaxe semblable à Haskell, bien qu’une sémantique bien différente de celle de Haskell):
mkCounter =
let x = newSlot
in (\() -> let old = get x
in get (put x (old + 1)))
Dans ce cas, nous utilisons des procédures qui manipulent cet emplacement mutable. Afin de l'implémenter, nous devrions fermer non seulement un environnement constant de noms, x
mais également un environnement mutable contenant tous les emplacements nécessaires. Ceci est plus proche de la notion commune de "fermeture" que les gens aiment tellement.
Encore une fois, mkCounter
c'est très impur. C'est aussi très opaque par rapport au référentiel. Mais remarquez que les effets secondaires ne découlent pas de la capture du nom ou de la fermeture du nom, mais plutôt de la capture de la cellule mutable et des opérations produisant des effets secondaires sur elle get
et put
.
En fin de compte, je pense que c'est la réponse finale à votre question: la pureté n'est pas affectée par la capture de variables (mathématiques), mais par les opérations à effets secondaires effectuées sur des emplacements mutables nommés par des variables capturées.
Ce n’est que dans les langues qui ne cherchent pas à être proches de LC ou à maintenir la pureté que ces deux concepts sont si souvent confondus, ce qui conduit à la confusion.