Ao ler o famoso SICP, achei os autores relutantes em apresentar a declaração de atribuição a Scheme no capítulo 3. Li o texto e meio que entendo por que eles sentem isso.
Como o Scheme é a primeira linguagem de programação funcional de que conheço alguma coisa, fico surpreso que haja algumas linguagens de programação funcionais (não é claro que Scheme) possam fazer sem atribuições.
Vamos usar o exemplo que o livro oferece, o bank account
exemplo. Se não houver declaração de atribuição, como isso pode ser feito? Como alterar a balance
variável? Eu pergunto isso porque sei que existem algumas chamadas linguagens funcionais puras por aí e, de acordo com a teoria completa de Turing, isso também pode ser feito.
Aprendi C, Java, Python e uso muito as atribuições em todos os programas que escrevi. Portanto, é realmente uma experiência reveladora. Eu realmente espero que alguém possa explicar brevemente como as atribuições são evitadas nessas linguagens de programação funcionais e que impacto profundo (se houver) sobre essas linguagens.
O exemplo mencionado acima está aqui:
(define (make-withdraw balance)
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")))
Isso mudou o balance
por set!
. Para mim, parece um método de classe para alterar o membro da classe balance
.
Como eu disse, não estou familiarizado com linguagens de programação funcionais; portanto, se eu disse algo errado sobre elas, sinta-se à vontade para apontar.
set!
vários programas de esquema sem usar ou outras funções que terminam com um!
. Quando estiver satisfeito com isso, a transição para o FP puro deve ser mais fácil.Respostas:
Você não pode alterar variáveis sem algum tipo de operador de atribuição.
Não é bem assim. Se um idioma é Turing completo, significa que ele pode calcular qualquer coisa que qualquer outro idioma completo de Turing possa calcular. Isso não significa que ele precisa ter todos os recursos que outros idiomas possuem.
Não é uma contradição que uma linguagem de programação completa de Turing não tenha como alterar o valor de uma variável, desde que, para todo programa que possua variáveis mutáveis, você possa escrever um programa equivalente que não possua variáveis mutáveis (onde "equivalente" significa que calcula a mesma coisa). E, de fato, todo programa pode ser escrito dessa maneira.
Em relação ao seu exemplo: em uma linguagem puramente funcional, você simplesmente não seria capaz de escrever uma função que retorna um saldo de conta diferente cada vez que é chamada. Mas você ainda poderá reescrever todos os programas que usam essa função de uma maneira diferente.
Como você pediu um exemplo, vamos considerar um programa imperativo que usa sua função de fazer e retirar (em pseudo-código). Este programa permite ao usuário retirar uma conta, depositar nela ou consultar a quantia em dinheiro na conta:
Aqui está uma maneira de escrever o mesmo programa sem usar variáveis mutáveis (não vou me preocupar com E / S referencialmente transparente porque a pergunta não era sobre isso):
A mesma função também pode ser escrita sem o uso de recursão, usando uma dobra sobre a entrada do usuário (o que seria mais idiomático do que a recursão explícita), mas não sei se você já conhece as dobras, então escrevi em maneira que não usa nada que você ainda não conhece.
fonte
newBalance = startingBalance + sum(deposits) - sum(withdrawals)
.Você está certo que se parece muito com um método em um objeto. Isso porque é essencialmente o que é. A
lambda
função é um fechamento que puxa a variável externabalance
para seu escopo. Ter vários fechamentos que fechem sobre a (s) mesma (s) variável (s) externa (s) e ter vários métodos no mesmo objeto são duas abstrações diferentes para fazer exatamente a mesma coisa, e uma pode ser implementada em termos da outra se você entender os dois paradigmas.A maneira como as linguagens funcionais puras lidam com o estado é enganando. Por exemplo, em Haskell, se você quiser ler informações de uma fonte externa (que é não-determinística, é claro, e não fornecerá necessariamente o mesmo resultado duas vezes se você repeti-las), ele usa um truque de mônada para dizer "nós temos obtivemos essa outra variável fingida que representa o estado de todo o resto do mundo , e não podemos examiná-lo diretamente, mas a leitura de entrada é uma função pura que pega o estado do mundo externo e retorna a entrada determinística de que esse estado exato sempre renderizará, mais o novo estado do mundo exterior ". (Essa é uma explicação simplificada, é claro. Ler a maneira como ela realmente funciona vai quebrar seriamente o seu cérebro.)
Ou, no caso de um problema na sua conta bancária, em vez de atribuir um novo valor à variável, ele pode retornar o novo valor como resultado da função e, em seguida, o chamador deve lidar com isso em um estilo funcional, geralmente recriando quaisquer dados. que referencia esse valor com uma nova versão que contém o valor atualizado. (Não é uma operação tão volumosa quanto pode parecer se seus dados forem configurados com o tipo certo de estrutura em árvore.)
fonte
b = makeWithdraw(42); b(1); b(2); b(3); print(b(4))
você, você pode simplesmente fazer ob = 42; b1 = withdraw(b1, 1); b2 = withdraw(b1, 2); b3 = withdraw(b2, 3); print(withdraw(b3, 4));
quewithdraw
é simplesmente definido comowithdraw(balance, amount) = balance - amount
."Operadores de atribuição múltipla" é um exemplo de recurso de idioma que, de um modo geral, tem efeitos colaterais e é incompatível com algumas propriedades úteis de idiomas funcionais (como a avaliação lenta).
Isso, no entanto, não significa que a atribuição em geral seja incompatível com um estilo de programação funcional puro (veja esta discussão, por exemplo), nem significa que você não pode construir uma sintaxe que permita ações que se parecem com atribuições em geral, mas são implementados sem efeitos colaterais. Porém, criar esse tipo de sintaxe e escrever programas eficientes é demorado e difícil.
No seu exemplo específico, você está certo - o cenário! operador é uma atribuição. É não um operador sem efeito colateral, e é um lugar onde as quebras esquema com uma abordagem puramente funcional para programação.
Em última análise, qualquer linguagem puramente funcional vai ter de romper com o sometime abordagem puramente funcional - a grande maioria dos programas úteis fazer ter efeitos secundários. A decisão de onde fazê-lo é geralmente uma questão de conveniência, e os designers de linguagem tentarão dar ao programador a maior flexibilidade para decidir onde romper com uma abordagem puramente funcional, conforme apropriado para o domínio do programa e do problema.
fonte
Em uma linguagem puramente funcional, seria possível programar um objeto de conta bancária como uma função de transformador de fluxo. O objeto é considerado como uma função de um fluxo infinito de solicitações dos proprietários da conta (ou de quem quer que seja) para um fluxo potencialmente infinito de respostas. A função inicia com um saldo inicial e processa cada solicitação no fluxo de entrada para calcular um novo saldo, que é retornado à chamada recursiva para processar o restante do fluxo. (Lembro que o SICP discute o paradigma de transformador de fluxo em outra parte do livro.)
Uma versão mais elaborada desse paradigma é chamada de "programação reativa funcional" discutida aqui no StackOverflow .
A maneira ingênua de fazer transformadores de fluxo tem alguns problemas. É possível (de fato, muito fácil) escrever programas com erros que mantêm todos os pedidos antigos, desperdiçando espaço. Mais seriamente, é possível fazer com que a resposta à solicitação atual dependa de solicitações futuras. As soluções para esses problemas estão sendo trabalhadas atualmente. Neel Krishnaswami é a força por trás deles.
Disclaimer : Eu não pertenço à igreja de pura programação funcional. Na verdade, eu não pertenço a nenhuma igreja :-)
fonte
Não é possível tornar um programa 100% funcional se ele deve fazer algo útil. (Se os efeitos colaterais não forem necessários, todo o pensamento poderá ter sido reduzido a um tempo de compilação constante). Como no exemplo de retirada, você pode tornar a maioria dos procedimentos funcionais, mas eventualmente precisará de procedimentos com efeitos colaterais (informações do usuário, saída para o console). Dito isto, você pode tornar a maior parte do seu código funcional e essa parte será fácil de testar, mesmo automaticamente. Então você cria um código imperativo para fazer a entrada / saída / banco de dados / ... que precisaria de depuração, mas manter a maior parte do código limpo não será muito trabalhoso. Vou usar seu exemplo de retirada:
É possível fazer o mesmo em quase qualquer idioma e produzir os mesmos resultados (menos bugs), embora você possa ter que definir variáveis temporárias em um procedimento e até alterar as coisas, mas isso não importa tanto quanto o procedimento realmente funciona funcional (os parâmetros por si só determinam o resultado). Acredito que você se torne um programador melhor em qualquer idioma depois de ter programado um pouco o LISP :)
fonte
A atribuição é uma operação ruim porque divide o espaço de estado em duas partes, antes da atribuição e após a atribuição. Isso causa dificuldades no rastreamento de como as variáveis estão sendo alteradas durante a execução do programa. O seguinte nas linguagens funcionais está substituindo atribuições:
fonte