Muito rapidamente: uma substituição é "referencialmente transparente" se "substituir igual leva a gostar" e uma função é "pura" se todos os seus efeitos estiverem contidos em seu valor de retorno. Ambos podem ser precisos, mas é vital notar que eles não são idênticos nem que um implica o outro.
Agora vamos falar sobre fechamentos.
"Encerramentos" chatos (principalmente puros)
Os fechamentos ocorrem porque, ao avaliarmos um termo lambda, interpretamos variáveis (vinculadas) como pesquisas de ambiente. Assim, quando retornarmos um termo lambda como resultado de uma avaliação, as variáveis dentro dele "fecharão" os valores que eles assumiram quando ele foi definido.
No cálculo lambda simples, isso é trivial e toda a noção simplesmente desaparece. Para demonstrar isso, aqui está um interpretador de cálculo lambda relativamente leve:
-- 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)
A parte importante a ser observada é addEnv
quando aumentamos o ambiente com um novo nome. Essa função é chamada apenas "dentro" do Abs
termo de tração interpretado (termo lambda). O ambiente é "procurado" sempre que avaliamos um Var
termo e, portanto, eles Var
decidem o que quer que seja Name
referido no Env
que foi capturado pela Abs
tração que contém o Var
.
Agora, novamente, em termos simples de LC, isso é chato. Isso significa que variáveis ligadas são apenas constantes, na medida em que alguém se importa. Eles são avaliados direta e imediatamente como os valores que eles denotam no ambiente, com o escopo lexicamente até aquele momento.
Isso também é (quase) puro. O único significado de qualquer termo em nosso cálculo lambda é determinado pelo seu valor de retorno. A única exceção é o efeito colateral da não rescisão, que é incorporado pelo termo 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")))
Fechamentos interessantes (impuros)
Agora, para certos antecedentes, os fechamentos descritos na LC simples acima são chatos porque não há noção de poder interagir com as variáveis que encerramos. Em particular, a palavra "encerramento" tende a invocar código como o seguinte 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
Isso demonstra que fechamos a n
variável na função interna incr
e a chamada incr
interage significativamente com essa variável. mk_counter
é puro, mas incr
é decididamente impuro (e também não é referencialmente transparente).
O que difere entre essas duas instâncias?
Noções de "variável"
Se observarmos o que substituição e abstração significam no sentido claro de LC, notamos que elas são decididamente claras. Variáveis literalmente nada mais são do que pesquisas imediatas no ambiente. A abstração do Lambda nada mais é do que criar um ambiente aumentado para avaliar a expressão interna. Não há espaço neste modelo para o tipo de comportamento que vimos com mk_counter
/ incr
porque não há variação permitida.
Para muitos, esse é o cerne do que "variável" significa - variação. No entanto, os semanticistas gostam de distinguir entre o tipo de variável usada na LC e o tipo de "variável" usada no Javascript. Para fazer isso, eles tendem a chamar o último de "célula mutável" ou "slot".
Essa nomenclatura segue o longo uso histórico de "variável" em matemática, onde significava algo mais parecido com "desconhecido": a expressão (matemática) "x + x" não permite x
variar ao longo do tempo, mas sim ter significado independentemente do valor (único, constante) x
leva.
Assim, dizemos "slot" para enfatizar a capacidade de colocar valores em um slot e removê-los.
Para aumentar ainda mais a confusão, em Javascript esses "slots" têm a mesma aparência de variáveis: escrevemos
var x;
para criar um e depois quando escrevemos
x;
isso indica que estamos pesquisando o valor atualmente armazenado nesse slot. Para tornar isso mais claro, linguagens puras tendem a pensar em slots como nomes (matemáticos, cálculo lambda). Nesse caso, devemos rotular explicitamente quando obtemos ou colocamos um slot. Essa notação tende a parecer
-- 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
A vantagem dessa notação é que agora temos uma firme distinção entre variáveis matemáticas e slots mutáveis. Variáveis podem usar slots como seus valores, mas o slot específico nomeado por uma variável é constante em todo o seu escopo.
Usando essa notação, podemos reescrever o mk_counter
exemplo (desta vez em uma sintaxe semelhante a Haskell, embora semântica decididamente não semelhante a Haskell):
mkCounter =
let x = newSlot
in (\() -> let old = get x
in get (put x (old + 1)))
Neste caso, estamos usando procedimentos que manipulam esse slot mutável. Para implementá-lo, precisamos fechar não apenas um ambiente constante de nomes como x
também um ambiente mutável que contenha todos os slots necessários. Isso está mais próximo da noção comum de "fechamento" que as pessoas amam tanto.
Mais uma vez, mkCounter
é muito impuro. Também é muito referencialmente opaco. Mas observe que os efeitos colaterais não surgem da captura ou fechamento do nome, mas da captura da célula mutável e das operações de efeito colateral, como get
e put
.
Por fim, acho que esta é a resposta final para sua pergunta: a pureza não é afetada pela captura de variáveis (matemáticas), mas pelas operações de efeito colateral executadas em slots mutáveis denominados por variáveis capturadas.
É apenas em linguagens que não tentam se aproximar da LC ou não tentam manter a pureza que esses dois conceitos são tão frequentemente conflitantes que levam à confusão.