Os fechamentos são considerados impuros na programação funcional?
Parece que geralmente se pode evitar fechamentos passando valores diretamente para uma função. Portanto, os fechamentos devem ser evitados sempre que possível?
Se eles são impuros, e eu estou correto ao afirmar que eles podem ser evitados, por que tantas linguagens de programação funcionais suportam fechamentos?
Um dos critérios para uma função pura é que "A função sempre avalia o mesmo valor de resultado, dados os mesmos valores de argumento ".
Suponha
f: x -> x + y
f(3)
nem sempre dará o mesmo resultado. f(3)
depende do valor do y
qual não é um argumento f
. Portanto, f
não é uma função pura.
Como todos os fechamentos dependem de valores que não são argumentos, como é possível que qualquer fechamento seja puro? Sim, em teoria, o valor fechado pode ser constante, mas não há como saber isso apenas olhando o código-fonte da própria função.
Onde isso me leva é que a mesma função pode ser pura em uma situação, mas impura em outra. Nem sempre se pode determinar se uma função é pura ou não, estudando seu código-fonte. Em vez disso, pode ser necessário considerá-lo no contexto de seu ambiente no momento em que está sendo chamado antes que essa distinção possa ser feita.
Estou pensando nisso corretamente?
fonte
y
não pode mudar, portanto a saída def(3)
sempre será a mesma.y
faz parte da definição def
, embora não esteja explicitamente marcado como uma entrada paraf
- ainda é o casof
definido em termos dey
(poderíamos denotar a função f_y, para tornar a dependênciay
explícita) e, portanto, alterary
fornece uma função diferente . A função específicaf_y
definida para um particulary
é muito pura. (Por exemplo, as duas funçõesf: x -> x + 3
ef: x -> x + 5
são diferentes funções, e ambos pura, mesmo que aconteceu a usar a mesma letra para denotar-los.)Respostas:
A pureza pode ser medida por duas coisas:
Se a resposta para 1 for sim e a resposta para 2 for não, então a função é pura. Os fechamentos apenas tornam uma função impura se você modificar a variável fechada.
fonte
y
. Assim, por exemplo, definimos uma funçãog
queg(y)
é ela mesma a funçãox -> x + y
. Entãog
é uma função de números inteiros que retorna funções,g(3)
é uma função de números inteiros que retorna números inteiros eg(2)
é uma função diferente de números inteiros que retorna números inteiros. Todas as três funções são puras.Os fechamentos aparecem Lambda Calculus, que é a forma mais pura de programação funcional possível, então eu não os chamaria de "impuros" ...
Os fechamentos não são "impuros" porque as funções em linguagens funcionais são cidadãos de primeira classe - isso significa que podem ser tratados como valores.
Imagine o seguinte (pseudocódigo):
y
é um valor. Seu valor dependex
, masx
é imutável, portantoy
, o valor também é imutável. Podemos chamarfoo
muitas vezes com argumentos diferentes que produzirãoy
s diferentes , masy
todos eles vivem em escopos diferentes e dependem dex
s diferentes, para que a pureza permaneça intacta.Agora vamos mudar:
Aqui estamos usando um fechamento (estamos fechando sobre x), mas é exatamente o mesmo que em
foo
- chamadas diferentes parabar
com argumentos diferentes criam valores diferentes dey
(lembre-se - funções são valores) que são todos imutáveis, portanto a pureza permanece intacta.Além disso, observe que os fechamentos têm um efeito muito semelhante ao curry:
baz
não é realmente diferente debar
- em ambos, criamos um valor de função nomeadoy
que retorna seu argumento maisx
. De fato, no Lambda Calculus você usa fechamentos para criar funções com vários argumentos - e ainda não é impuro.fonte
Outros abordaram bem a questão geral em suas respostas, portanto, examinarei apenas a confusão que você sinaliza em sua edição.
O fechamento não se torna uma entrada da função, mas 'entra' no corpo da função. Para ser mais concreto, uma função refere-se a um valor no escopo externo de seu corpo.
Você tem a impressão de que ela torna a função impura. Esse não é o caso, em geral. Na programação funcional, os valores são imutáveis na maioria das vezes . Isso também se aplica ao valor fechado.
Digamos que você tenha um código como este:
Chamar
make 3
emake 4
fornecerá duas funções com encerramentos sobremake
oy
argumento. Um deles retornaráx + 3
, o outrox + 4
. No entanto, são duas funções distintas e ambas são puras. Eles foram criados usando a mesmamake
função, mas é isso.Observe na maioria das vezes alguns parágrafos atrás.
Observe que, para 2 e 3, esses idiomas não oferecem garantias sobre pureza. A impureza não é propriedade do fechamento, mas da própria linguagem. Os fechamentos não mudam muito a imagem por si mesmos.
fonte
IO A
valor imutável e seu tipo de fechamento éIO (B -> C)
ou algo assim. A pureza é mantidaNormalmente, peço que você esclareça sua definição de "impuro", mas, neste caso, isso realmente não importa. Supondo que você esteja contrastando com o termo puramente funcional , a resposta é "não", porque não há nada sobre fechamentos que seja inerentemente destrutivo. Se o seu idioma fosse puramente funcional sem encerramentos, ainda seria puramente funcional com os encerramentos. Se, em vez disso, você quer dizer "não funcional", a resposta ainda é "não"; fechamentos facilitam a criação de funções.
Sim, mas sua função teria mais um parâmetro e isso mudaria de tipo. Os fechamentos permitem criar funções baseadas em variáveis sem adicionar parâmetros. Isso é útil quando você tem, digamos, uma função que usa 2 argumentos e deseja criar uma versão dela que usa apenas 1 argumento.
EDIT: No que diz respeito à sua própria edição / exemplo ...
Depende é a escolha errada da palavra aqui. Citando o mesmo artigo da Wikipedia que você fez:
Supondo que
y
seja imutável (que geralmente é o caso em linguagens funcionais), a condição 1 é satisfeita: para todos os valores dex
, o valor def(x)
não muda. Isso deve ficar claro pelo fato de quey
não é diferente de uma constante ex + 3
é puro. Também está claro que não há mutação ou E / S acontecendo.fonte
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:
A parte importante a ser observada é
addEnv
quando aumentamos o ambiente com um novo nome. Essa função é chamada apenas "dentro" doAbs
termo de tração interpretado (termo lambda). O ambiente é "procurado" sempre que avaliamos umVar
termo e, portanto, elesVar
decidem o que quer que sejaName
referido noEnv
que foi capturado pelaAbs
tração que contém oVar
.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:
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
Isso demonstra que fechamos a
n
variável na função internaincr
e a chamadaincr
interage significativamente com essa variável.mk_counter
é puro, masincr
é 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
para criar um e depois quando escrevemos
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
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):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, comoget
eput
.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.
fonte
Não, os fechamentos não fazem com que uma função seja impura, desde que o valor fechado seja constante (nem alterado pelo fechamento nem por outro código), como é o caso usual na programação funcional.
Observe que, embora você sempre possa transmitir um valor como argumento, normalmente não pode fazê-lo sem uma quantidade considerável de dificuldade. Por exemplo (coffeescript):
Por sua sugestão, você pode simplesmente retornar:
Como essa função não está sendo chamada neste momento, apenas definida , você deve encontrar uma maneira de passar o valor desejado para
closedValue
o ponto em que a função está realmente sendo chamada. Na melhor das hipóteses, isso cria muito acoplamento. Na pior das hipóteses, você não controla o código no ponto de chamada, portanto é efetivamente impossível.As bibliotecas de eventos em idiomas que não oferecem suporte a fechamentos geralmente oferecem outra maneira de passar dados arbitrários de volta para o retorno de chamada, mas não é bonito e cria muita complexidade para o mantenedor da biblioteca e os usuários da biblioteca.
fonte