como uma linguagem de programação funcional pura gerencia sem instruções de atribuição?


26

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 accountexemplo. Se não houver declaração de atribuição, como isso pode ser feito? Como alterar a balancevariá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 balancepor 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.


1
Em relação à aprendizagem de uma linguagem puramente funcional: eu não recomendaria necessariamente fazer isso imediatamente. Se você aprender Haskell, além de aprender a escrever programas sem variáveis ​​mutáveis, também precisará aprender sobre preguiça e a maneira de Haskell executar IO. Isso pode ser um pouco demais ao mesmo tempo. Se você deseja aprender a escrever programas sem estado mutável, a maneira mais fácil provavelmente seria escrever 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.
sepp2k

Respostas:


21

Se não houver declaração de atribuição, como isso pode ser feito? Como modificar a variável de saldo?

Você não pode alterar variáveis ​​sem algum tipo de operador de atribuição.

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.

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:

account = make-withdraw(0)
ask for input until the user enters "quit"
    if the user entered "withdraw $x"
        account(x)
    if the user entered "deposit $x"
        account(-x)
    if the user entered "query"
        print("The balance of the account is " + account(0))

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):

function IO_loop(balance):
    ask for input
    if the user entered "withdraw $x"
        IO_loop(balance - x)
    if the user entered "deposit $x"
        IO_loop(balance + x)
    if the user entered "query"
        print("The balance of the account is " + balance)
        IO_loop(balance)
    if the user entered "quit"
        do nothing

 IO_loop(0)

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.


Entendo o seu ponto de vista, mas vamos ver que eu quero um programa que também simule a conta bancária e também faça essas coisas (retirada e depósito). Existe alguma maneira fácil de fazer isso?
Gnijuohz

@Gnijuohz Depende sempre de qual problema exatamente você está tentando resolver. Por exemplo, se você possui um saldo inicial e uma lista de saques e depósitos e deseja saber o saldo após esses saques e depósitos, pode simplesmente calcular a soma dos depósitos menos a soma dos saques e adicioná-la ao saldo inicial . Então, no código que seria newBalance = startingBalance + sum(deposits) - sum(withdrawals).
sepp2k

1
@Gnijuohz Adicionei um exemplo de programa à minha resposta.
sepp2k

Obrigado pelo tempo e esforço que você dedicou a escrever e reescrever a resposta! :)
Gnijuohz

Gostaria de acrescentar que o uso de continuação também poderia ser um meio para conseguir isso no esquema (contanto que você pode passar um argumento para a continuação?)
dader51

11

Você está certo que se parece muito com um método em um objeto. Isso porque é essencialmente o que é. A lambdafunçã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.)


Eu estou realmente interessado em nossa resposta e o exemplo de Haskell, mas devido à falta de conhecimento sobre o assunto eu não posso entender completamente a última parte de sua resposta (bem, também a segunda parte :()
Gnijuohz

3
@Gnijuohz O último parágrafo está dizendo que, em vez de b = makeWithdraw(42); b(1); b(2); b(3); print(b(4))você, você pode simplesmente fazer o b = 42; b1 = withdraw(b1, 1); b2 = withdraw(b1, 2); b3 = withdraw(b2, 3); print(withdraw(b3, 4));que withdrawé simplesmente definido como withdraw(balance, amount) = balance - amount.
sepp2k

3

"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.


"Em última análise, qualquer linguagem puramente funcional terá que romper com a abordagem puramente funcional em algum momento - a grande maioria dos programas úteis tem efeitos colaterais" É verdade, mas você está falando sobre fazer IO e tal. Muitos programas úteis podem ser escritos sem variáveis ​​mutáveis.
sepp2k

1
... e com "a grande maioria" de programas úteis, você quer dizer "todos", certo? Estou tendo dificuldade em imaginar a possibilidade da existência de qualquer programa que possa ser razoavelmente chamado de "útil" que não execute E / S, um ato que requer efeitos colaterais em ambas as direções.
Mason Wheeler

Os programas @MasonWheeler SQL não fazem IO como tal. Também não é incomum escrever várias funções que não executam E / S em um idioma que possui um REPL e, em seguida, simplesmente chamá-las de um REPL. Isso pode ser perfeitamente útil se o seu público-alvo for capaz de usar o REPL (especialmente se o seu público-alvo for você).
sepp2k

1
@MasonWheeler: apenas um contra-exemplo simples e simples: o cálculo conceitual de n dígitos de pi não requer E / S. É "apenas" matemática e variáveis. A única entrada necessária é n e o valor de retorno é Pi (para n dígitos).
Joachim Sauer

1
@Joachim Sauer eventualmente, você desejará imprimir o resultado na tela ou informar o usuário sobre o resultado. E, inicialmente, você desejará carregar algumas constantes no programa de algum lugar. Então, se você quiser ser pedante, todos os programas úteis tem que fazer IO em algum momento, mesmo que seja casos triviais que estão implícitos e sempre escondida do programador pelo ambiente
blueberryfields

3

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 :-)


Eu acho que você pertence a algum templo? :-P
Gnijuohz 16/04

1
O templo do pensamento livre. Não há pregadores lá.
Uday Reddy

2

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:

(define +no-founds+ "Insufficient funds")

;; functional withdraw
(define (make-withdraw balance amount)
    (if (>= balance amount)
        (- balance amount)
        +no-founds+))

;; functional atm loop
(define (atm balance thunk)
  (let* ((amount (thunk balance)) 
         (new-balance (make-withdraw balance amount)))
    (if (eqv? new-balance +no-founds+)
        (cons +no-founds+ '())
        (cons (list 'withdraw amount 'balance new-balance) (atm new-balance thunk)))))

;; functional balance-line -> string 
(define (balance->string x)
  (if (eqv? x +no-founds+)
      (string-append +no-founds+ "\n")
      (if (null? x)
          "\n"
          (let ((first-token (car x)))
            (string-append
             (cond ((symbol? first-token) (symbol->string first-token))
                   (else (number->string first-token)))
             " "
             (balance->string (cdr x)))))))

;; functional thunk to test  
(define (input-10 x) 10) ;; define a purly functional input-method

;; since all procedures involved are functional 
;; we expect the same result every time.
;; we use this to test atm and make-withdraw
(apply string-append (map balance->string (atm 100 input-10)))

;; no program can be purly functional in any language.
;; From here on there are imperative dirty procedures!

;; A procedure to get input from user is needed. 
;; Side effects makes it imperative
(define (user-input balance)
  (display "You have $")
  (display balance)
  (display " founds. How much to withdraw? ")
  (read))

;; We need a procedure to print stuff to the console 
;; as well. Side effects makes it imperative
(define (pretty-print-result x)
  (for-each (lambda (x) (display (balance->string x))) x))

;; use imperative procedure with atm.
(pretty-print-result (atm 100 user-input))

É 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 :)


+1 para o exemplo abrangente e explicações realistas sobre partes funcionais e partes funcionais não puras do programa e mencionar o motivo pelo qual o FP é importante.
Zelphir Kaltstahl 23/09

1

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:

  1. Parâmetros de função vinculados diretamente aos valores de retorno
  2. escolhendo objetos diferentes a serem retornados em vez de modificar objetos existentes.
  3. criando novos valores avaliados preguiçosamente
  4. listando todos os objetos possíveis , não apenas aqueles que precisam estar na memória
  5. Sem efeitos colaterais

Isso não parece abordar a questão colocada. Como você programa um objeto de conta bancária em uma linguagem funcional pura?
Uday Reddy

são apenas funções que se transformam de um registro de conta bancária para outro. A chave é que, quando essas transformações acontecem, novos objetos são escolhidos em vez de modificar os existentes.
tp1

Quando você transforma um registro de conta bancária em outro, deseja que o cliente faça a próxima transação no novo registro, não no antigo. O "ponto de contato" do cliente deve ser atualizado constantemente para apontar para o registro atual. Essa é uma ideia fundamental de "modificação". "Objetos" da conta bancária não são registros da conta bancária.
perfil completo de Uday Reddy
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.