Como você pode fazer algo útil sem um estado mutável?


265

Ultimamente, tenho lido muitas coisas sobre programação funcional, e consigo entender a maior parte delas, mas a única coisa que não consigo entender é a codificação sem estado. Parece-me que simplificar a programação removendo o estado mutável é como "simplificar" um carro removendo o painel: o produto final pode ser mais simples, mas boa sorte ao interagir com os usuários finais.

Quase todos os aplicativos de usuário em que consigo pensar envolvem estado como um conceito central. Se você escrever um documento (ou uma publicação SO), o estado mudará a cada nova entrada. Ou, se você joga um videogame, existem inúmeras variáveis ​​de estado, começando pelas posições de todos os personagens, que tendem a se mover constantemente. Como você pode fazer algo útil sem acompanhar as alterações nos valores?

Toda vez que encontro algo que discute esse problema, ele está escrito em uma funcionalidade funcional realmente técnica que assume um background pesado de FP que eu não tenho. Alguém sabe uma maneira de explicar isso para alguém com uma compreensão sólida e boa da codificação imperativa, mas quem é um n00b completo no lado funcional?

Edição: Um monte de respostas até agora parecem estar tentando me convencer das vantagens de valores imutáveis. Eu entendo essa parte. Faz todo o sentido. O que não entendo é como você pode acompanhar os valores que precisam mudar e mudar constantemente, sem variáveis ​​mutáveis.



1
Minha humilde opinião pessoal é que é como força e dinheiro. A lei dos rendimentos decrescentes se aplica. Se você é bastante forte, pode haver pouco incentivo para ficar um pouco mais forte, mas não custa trabalhar nisso (e algumas pessoas o fazem com paixão). O mesmo se aplica ao estado mutável global. É minha preferência pessoal aceitar que, conforme minha habilidade de codificação progride, é bom limitar a quantidade de estado mutável global no meu código. Pode nunca ser perfeito, mas é bom trabalhar no sentido de minimizar o estado mutável global.
AturSams

Assim como no dinheiro, um ponto será alcançado quando investir mais tempo nele, não for mais muito útil e outras prioridades subirão ao topo. Se, por exemplo, você atingir a maior quantidade de força possível (de acordo com minha metáfora), isso pode não servir a nenhum propósito útil e pode até se tornar um fardo. Mas ainda é bom se esforçar para atingir esse objetivo possivelmente inatingível e investir recursos moderados nele.
AturSams

7
Resumidamente, no FP, as funções nunca modificam o estado. Eventualmente, eles retornarão algo que substitui o estado atual. Mas o estado nunca é modificado (mutado) no local.
Jinglesthula 28/03

Existem maneiras de obter um estado sem mutação (usando a pilha do que eu entendo), mas essa questão está de certa forma fora de questão (mesmo que seja ótima). Difícil falar sucintamente, mas aqui está um post que espero responder à sua pergunta medium.com/@jbmilgrom/… . O TLDR é que a semântica mesmo de um programa funcional com estado é imutável, no entanto, a comunicação entre as execuções em preto e branco da função de programa é manipulada.
jbmilgrom 10/03

Respostas:


166

Ou, se você joga um videogame, existem inúmeras variáveis ​​de estado, começando pelas posições de todos os personagens, que tendem a se mover constantemente. Como você pode fazer algo útil sem acompanhar as alterações nos valores?

Se você estiver interessado, aqui está uma série de artigos que descrevem a programação de jogos com Erlang.

Você provavelmente não gostará desta resposta, mas não receberá um programa funcional até usá-la. Eu posso postar exemplos de código e dizer "Aqui, você não " - mas se você não entende a sintaxe e os princípios subjacentes, seus olhos simplesmente brilham. Do seu ponto de vista, parece que estou fazendo a mesma coisa que uma linguagem imperativa, mas apenas configurando todos os tipos de limites para propositalmente dificultar a programação. Meu ponto de vista, você está apenas experimentando o paradoxo de Blub .

Eu fiquei cético no começo, mas entrei no trem de programação funcional há alguns anos e me apaixonei por ele. O truque da programação funcional é poder reconhecer padrões, atribuições de variáveis ​​específicas e mover o estado imperativo para a pilha. Um loop for, por exemplo, torna-se recursão:

// Imperative
let printTo x =
    for a in 1 .. x do
        printfn "%i" a

// Recursive
let printTo x =
    let rec loop a = if a <= x then printfn "%i" a; loop (a + 1)
    loop 1

Não é muito bonito, mas temos o mesmo efeito sem mutação. Obviamente, sempre que possível, gostamos de evitar repetições e simplesmente abstraí-lo:

// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)

O método Seq.iter enumerará através da coleção e chamará a função anônima para cada item. Muito conveniente :)

Eu sei, imprimir números não é exatamente impressionante. No entanto, podemos usar a mesma abordagem nos jogos: mantenha todo o estado na pilha e crie um novo objeto com nossas alterações na chamada recursiva. Dessa forma, cada quadro é um instantâneo sem estado do jogo, onde cada quadro simplesmente cria um novo objeto com as alterações desejadas de qualquer objeto sem estado que precise ser atualizado. O pseudocódigo para isso pode ser:

// imperative version
pacman = new pacman(0, 0)
while true
    if key = UP then pacman.y++
    elif key = DOWN then pacman.y--
    elif key = LEFT then pacman.x--
    elif key = UP then pacman.x++
    render(pacman)

// functional version
let rec loop pacman =
    render(pacman)
    let x, y = switch(key)
        case LEFT: pacman.x - 1, pacman.y
        case RIGHT: pacman.x + 1, pacman.y
        case UP: pacman.x, pacman.y - 1
        case DOWN: pacman.x, pacman.y + 1
    loop(new pacman(x, y))

As versões imperativa e funcional são idênticas, mas a versão funcional claramente não usa estado mutável. O código funcional mantém todo o estado mantido na pilha - o bom dessa abordagem é que, se algo der errado, a depuração é fácil, tudo o que você precisa é de um rastreamento de pilha.

Isso aumenta para qualquer número de objetos no jogo, porque todos os objetos (ou coleções de objetos relacionados) podem ser renderizados em seu próprio encadeamento.

Quase todos os aplicativos de usuário em que consigo pensar envolvem estado como um conceito central.

Nas linguagens funcionais, em vez de alterar o estado dos objetos, simplesmente retornamos um novo objeto com as alterações que desejamos. É mais eficiente do que parece. Estruturas de dados, por exemplo, são muito fáceis de representar como estruturas de dados imutáveis. Pilhas, por exemplo, são notoriamente fáceis de implementar:

using System;

namespace ConsoleApplication1
{
    static class Stack
    {
        public static Stack<T> Cons<T>(T hd, Stack<T> tl) { return new Stack<T>(hd, tl); }
        public static Stack<T> Append<T>(Stack<T> x, Stack<T> y)
        {
            return x == null ? y : Cons(x.Head, Append(x.Tail, y));
        }
        public static void Iter<T>(Stack<T> x, Action<T> f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } }
    }

    class Stack<T>
    {
        public readonly T Head;
        public readonly Stack<T> Tail;
        public Stack(T hd, Stack<T> tl)
        {
            this.Head = hd;
            this.Tail = tl;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Stack<int> x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null))));
            Stack<int> y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null))));
            Stack<int> z = Stack.Append(x, y);
            Stack.Iter(z, a => Console.WriteLine(a));
            Console.ReadKey(true);
        }
    }
}

O código acima constrói duas listas imutáveis, as anexa para criar uma nova lista e os resultados. Nenhum estado mutável é usado em qualquer lugar do aplicativo. Parece um pouco volumoso, mas isso é apenas porque o C # é uma linguagem detalhada. Aqui está o programa equivalente em F #:

type 'a stack =
    | Cons of 'a * 'a stack
    | Nil

let rec append x y =
    match x with
    | Cons(hd, tl) -> Cons(hd, append tl y)
    | Nil -> y

let rec iter f = function
    | Cons(hd, tl) -> f(hd); iter f tl
    | Nil -> ()

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
let y = Cons(5, Cons(6, Cons(7, Cons(8, Nil))))
let z = append x y
iter (fun a -> printfn "%i" a) z

Não é necessário mutável para criar e manipular listas. Quase todas as estruturas de dados podem ser facilmente convertidas em seus equivalentes funcionais. Eu escrevi uma página aqui que fornece implementações imutáveis ​​de pilhas, filas, montes de esquerda, árvores vermelho-pretas, listas preguiçosas. Nenhum trecho de código contém um estado mutável. Para "modificar" uma árvore, crio uma nova com o novo nó que desejo - isso é muito eficiente porque não preciso fazer uma cópia de todos os nós da árvore, posso reutilizar os antigos no meu novo árvore.

Usando um exemplo mais significativo, também escrevi esse analisador SQL totalmente sem estado (ou pelo menos meu código é sem estado, não sei se a biblioteca lexing subjacente é sem estado).

A programação sem estado é tão expressiva e poderosa quanto a programação com estado, requer apenas um pouco de prática para se treinar para começar a pensar sem estado. Obviamente, "programação sem estado, quando possível, programação com estado sempre que necessário" parece ser o lema da maioria das linguagens funcionais impuras. Não há mal nenhum em recorrer a mutáveis ​​quando a abordagem funcional simplesmente não é tão limpa ou eficiente.


7
Eu gosto do exemplo do Pacman. Mas isso poderia resolver um problema apenas para suscitar outro: e se outra coisa contiver uma referência ao objeto Pacman existente? Então não será coletado e substituído pelo lixo; em vez disso, você acaba com duas cópias do objeto, uma das quais é inválida. Como você lida com esse problema?
Mason Wheeler

9
Obviamente, você precisa criar um novo "algo mais" com o novo objeto Pacman;) Obviamente, se levarmos esse caminho longe demais, acabamos recriando o gráfico de objetos para o mundo inteiro toda vez que algo muda. Uma abordagem melhor é descrita aqui ( prog21.dadgum.com/26.html ): em vez de os objetos serem atualizados e todas as suas dependências, é muito mais fácil fazer com que eles passem mensagens sobre seu estado para um loop de eventos que lida com todos os atualização. Isso facilita muito a decisão de quais objetos no gráfico precisam ser atualizados e quais não.
Julieta

6
@ Juliet, tenho uma dúvida - na minha mentalidade totalmente imperativa, a recursão deve terminar em algum momento, caso contrário você acabará produzindo um estouro de pilha. No exemplo pacman recursivo, como a pilha é mantida afastada - o objeto é implicitamente exibido no início da função?
BlueStrat

9
@BlueStrat - boa pergunta ... se é uma "chamada final" ... ou seja, a chamada recursiva é a última coisa na função ... então o sistema não precisa gerar um novo quadro de pilha ... pode apenas reutilize o anterior. Essa é uma otimização comum para linguagens de programação funcionais. pt.wikipedia.org/wiki/Tail_call
reteptilian 27/10

4
@MichaelOsofsky, ao interagir com bancos de dados e APIs, sempre há um 'mundo externo' com estado para se comunicar. Nesse caso, você não pode ser 100% funcional. É importante manter esse código "não funcional" isolado e abstrato, para que haja apenas uma entrada e uma saída para o mundo externo. Dessa forma, você pode manter o restante do seu código funcional.
Chielt

76

Resposta curta: você não pode.

Então, qual é o problema da imutabilidade?

Se você é versado em linguagem imperativa, sabe que "os globais são ruins". Por quê? Porque eles introduzem (ou têm o potencial de introduzir) algumas dependências muito difíceis de desembaraçar no seu código. E dependências não são boas; você deseja que seu código seja modular . Partes do programa não influenciam outras partes o menos possível. E FP traz para o santo graal da modularidade: sem efeitos colaterais em tudo . Você apenas tem seu f (x) = y. Coloque x, tire y. Nenhuma alteração em x ou qualquer outra coisa. FP faz você parar de pensar em estado e começar a pensar em termos de valores. Todas as suas funções simplesmente recebem valores e produzem novos valores.

Isso tem várias vantagens.

Primeiro, nenhum efeito colateral significa programas mais simples, mais fáceis de raciocinar. Não é necessário se preocupar que a introdução de uma nova parte do programa interfira e trava uma parte já existente.

Segundo, isso torna o programa trivialmente paralelelizável (paralelização eficiente é outra questão).

Terceiro, existem algumas vantagens de desempenho possíveis. Digamos que você tenha uma função:

double x = 2 * x

Agora você coloca um valor de 3 in e obtém um valor de 6 out. Toda vez. Mas você pode fazer isso também de maneira imperativa, certo? Sim. Mas o problema é que, em imperativo, você pode fazer ainda mais . Eu posso fazer:

int y = 2;
int double(x){ return x * y; }

mas eu também poderia fazer

int y = 2;
int double(x){ return x * (y++); }

O compilador imperativo não sabe se terei efeitos colaterais ou não, o que dificulta a otimização (ou seja, o dobro 2 não precisa ser 4 toda vez). O funcional sabe que não - portanto, ele pode otimizar toda vez que vir o "duplo 2".

Agora, mesmo que a criação de novos valores sempre pareça incrivelmente inútil para tipos complexos de valores em termos de memória do computador, não precisa ser assim. Porque, se você tem f (x) = y, e os valores x e y são "basicamente os mesmos" (por exemplo, árvores que diferem apenas em algumas folhas), x e y podem compartilhar partes da memória - porque nenhuma delas sofrerá mutação .

Então, se essa coisa imutável é tão boa, por que eu respondi que você não pode fazer nada útil sem um estado mutável. Bem, sem mutabilidade, todo o seu programa seria uma função f (x) = y gigante. E o mesmo vale para todas as partes do seu programa: apenas funções e funções no sentido "puro". Como eu disse, isso significa f (x) = y sempre . Então, por exemplo, readFile ("myFile.txt") precisaria retornar o mesmo valor de string sempre. Não é muito útil.

Portanto, todo FP fornece alguns meios de estado de mutação. Linguagens funcionais "puras" (por exemplo, Haskell) fazem isso usando conceitos um tanto assustadores, como mônadas, enquanto que as "impuras" (por exemplo, ML) permitem isso diretamente.

E, é claro, as linguagens funcionais vêm com uma série de outras vantagens que tornam a programação mais eficiente, como funções de primeira classe, etc.


2
<< readFile ("myFile.txt") precisaria retornar o mesmo valor de string sempre. Não é muito útil. >> Acho que é útil enquanto você oculta o sistema de arquivos global. Se você o considerar como um segundo parâmetro e permitir que outros processos retornem uma nova referência ao sistema de arquivos sempre que modificá-lo com filesystem2 = write (filesystem1, fd, pos, "string") e permita que todos os processos troquem sua referência ao sistema de arquivos , poderíamos ter uma imagem muito mais limpa do sistema operacional.
eel ghEEz

@eelghEEz, essa é a mesma abordagem que o Datomic adota nos bancos de dados.
Jason

1
+1 para a comparação clara e concisa entre paradigmas. Uma sugestão é int double(x){ return x * (++y); }desde o atual ainda será 4, apesar de ainda ter um efeito colateral não divulgada, enquanto que ++yvoltará 6.
BrainFRZ

@eelghEEz Não tenho certeza de uma alternativa, realmente, tem mais alguém? Para introduzir informações em um contexto de FP (puro), você "faz uma medição", por exemplo "no registro de data e hora X, a temperatura é Y". Se alguém pede a temperatura, pode implicitamente significar X = agora, mas não pode estar pedindo a temperatura como uma função universal do tempo, certo? O FP lida com o estado imutável, e você deve criar um estado imutável - a partir de fontes internas e externas - a partir de um estado mutável. Índices, timestamps, etc. são úteis, mas ortogonais à mutabilidade - como os VCS são para controlar a versão em si.
John P

29

Observe que dizer que a programação funcional não tem 'estado' é um pouco enganador e pode ser a causa da confusão. Definitivamente, não possui um 'estado mutável', mas ainda pode ter valores manipulados; eles simplesmente não podem ser alterados no local (por exemplo, você precisa criar novos valores a partir dos valores antigos).

Essa é uma simplificação grosseira, mas imagine que você tenha uma linguagem OO, em que todas as propriedades das classes são definidas apenas uma vez no construtor, todos os métodos são funções estáticas. Você ainda pode executar praticamente qualquer cálculo fazendo com que os métodos utilizem objetos que contenham todos os valores necessários para os cálculos e retornem novos objetos com o resultado (talvez até uma nova instância do mesmo objeto).

Pode ser 'difícil' traduzir o código existente nesse paradigma, mas isso é porque realmente requer uma maneira completamente diferente de pensar sobre o código. Como efeito colateral, na maioria dos casos, você tem muitas oportunidades de paralelismo de graça.

Adendo: (Em relação à sua edição de como acompanhar os valores que precisam ser alterados)
Eles seriam armazenados em uma estrutura de dados imutável, é claro ...

Esta não é uma 'solução' sugerida, mas a maneira mais fácil de ver que isso sempre funcionará é que você pode armazenar esses valores imutáveis ​​em uma estrutura de mapa (dicionário / hashtable), digitada por um 'nome de variável'.

Obviamente, em soluções práticas, você usaria uma abordagem mais sensata, mas isso mostra que, na pior das hipóteses, se nada mais funcionasse, você poderia "simular" o estado mutável com um mapa que você carrega pela árvore de invocação.


2
OK, eu mudei o título. Sua resposta parece levar a um problema ainda pior. Se eu tiver que recriar todos os objetos sempre que algo em seu estado mudar, gastarei todo o tempo da CPU fazendo nada além de construir objetos. Estou pensando em programação de jogos aqui, onde você tem muitas coisas se movendo na tela (e fora da tela) de uma só vez, que precisam ser capazes de interagir entre si. Todo o mecanismo tem uma taxa de quadros definida: tudo o que você fará, você deve fazer em um número X de milissegundos. Certamente existe uma maneira melhor do que reciclar constantemente objetos inteiros?
Mason Wheeler

4
A beleza disso é que a imutabilidade está na linguagem, não na implementação. Com alguns truques, você pode ter um estado imutável no idioma enquanto a implementação está de fato alterando o estado em vigor. Veja, por exemplo, a mônada ST de Haskell.
CesarB

4
@Mason: O ponto é que o compilador pode decidir muito melhor onde é seguro (thread) alterar o estado no local do que você pode.
jerryjvl

Eu acho que para jogos você deve evitar imutáveis ​​todas as partes em que a velocidade não importa. Embora uma linguagem imutável possa otimizar para você, nada será mais rápido do que modificar a memória que as CPUs são rápidas em fazer. E assim, se houver 10 ou 20 lugares onde você precisa, eu acho que você deve evitar imutáveis ​​por completo, a menos que você possa modularizá-lo para áreas muito separadas, como menus de jogos. E a lógica dos jogos em particular pode ser um bom lugar para usar imutável, porque eu acho que é ótimo para modelagem complexa de sistemas puros, como regras de negócios.
LegendLength

@LegendLength você está se contradizendo.
Ixx

18

Eu acho que há um pequeno mal-entendido. Programas funcionais puros têm estado. A diferença é como esse estado é modelado. Na programação funcional pura, o estado é manipulado por funções que pegam algum estado e retornam o próximo estado. A sequência através dos estados é alcançada passando o estado através de uma sequência de funções puras.

Até o estado mutável global pode ser modelado dessa maneira. Em Haskell, por exemplo, um programa é uma função de um mundo para um mundo. Ou seja, você passa por todo o universo , e o programa retorna um novo universo. Na prática, porém, você só precisa passar pelas partes do universo em que seu programa está realmente interessado. E os programas realmente retornam uma sequência de ações que servem como instruções para o ambiente operacional no qual o programa é executado.

Você queria ver isso explicado em termos de programação imperativa. OK, vejamos uma programação imperativa realmente simples em uma linguagem funcional.

Considere este código:

int x = 1;
int y = x + 1;
x = x + y;
return x;

Código imperativo bastante padrão. Não faz nada de interessante, mas tudo bem para ilustração. Eu acho que você concorda que há um estado envolvido aqui. O valor da variável x muda com o tempo. Agora, vamos mudar um pouco a notação inventando uma nova sintaxe:

let x = 1 in
let y = x + 1 in
let z = x + y in z 

Coloque parênteses para esclarecer o que isso significa:

let x = 1 in (let y = x + 1 in (let z = x + y in (z)))

Como você vê, o estado é modelado por uma sequência de expressões puras que ligam as variáveis ​​livres das seguintes expressões.

Você descobrirá que esse padrão pode modelar qualquer tipo de estado, mesmo IO.


Isso é como uma mônada?
CMCDragonkai

Você considera o seguinte: A é declarativo no nível 1 B é declarativo no nível 2, considera A imperativo. C é declarativo no nível 3, considera B imperativo. À medida que aumentamos a camada de abstração, ela sempre considera os idiomas mais baixos na camada de abstração como mais imperativos que ela mesma.
CMCDragonkai

14

Aqui está como você escreve código sem estado mutável : em vez de colocar a mudança de estado em variáveis ​​mutáveis, você o coloca nos parâmetros das funções. E, em vez de escrever loops, você escreve funções recursivas. Então, por exemplo, este código imperativo:

f_imperative(y) {
  local x;
  x := e;
  while p(x, y) do
    x := g(x, y)
  return h(x, y)
}

torna-se este código funcional (sintaxe semelhante ao esquema):

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

ou este código haskellish

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

Quanto ao motivo pelo qual os programadores funcionais gostam de fazer isso (o que você não pediu), quanto mais partes do seu programa não tiverem estado, mais maneiras existem de juntar as peças sem interromper nada . O poder do paradigma apátrida não reside na apatridia (ou pureza) per se , mas na capacidade que ele lhe dá de escrever textos poderosos e reutilizáveis funções e combiná-las.

Você pode encontrar um bom tutorial com muitos exemplos no artigo de John Hughes, Por que a Programação Funcional é Importante .


13

São apenas maneiras diferentes de fazer a mesma coisa.

Considere um exemplo simples, como adicionar os números 3, 5 e 10. Imagine pensar em fazer isso alterando primeiro o valor de 3 adicionando 5 a ele, acrescentando 10 ao "3" e, em seguida, exibindo o valor atual de " 3 "(18). Isso parece ridiculamente evidente, mas é essencialmente o modo como a programação imperativa baseada em estado é frequentemente feita. De fato, você pode ter muitos "3" diferentes que têm o valor 3, mas são diferentes. Tudo isso parece estranho, porque estamos tão arraigados com a idéia, bastante sensata, de que os números são imutáveis.

Agora pense em adicionar 3, 5 e 10 quando considerar que os valores são imutáveis. Você adiciona 3 e 5 para produzir outro valor, 8 e, em seguida, adiciona 10 a esse valor para produzir outro valor, 18.

Essas são maneiras equivalentes de fazer a mesma coisa. Todas as informações necessárias existem nos dois métodos, mas de formas diferentes. Em uma, a informação existe como estado e nas regras para mudar de estado. No outro, a informação existe em dados imutáveis ​​e definições funcionais.


10

Estou atrasado para a discussão, mas queria acrescentar alguns pontos para as pessoas que estão lutando com a programação funcional.

  1. Os idiomas funcionais mantêm exatamente as mesmas atualizações de estado que os idiomas imperativos, mas o fazem passando o estado atualizado para as chamadas de função subsequentes . Aqui está um exemplo muito simples de viajar por uma linha numérica. Seu estado é sua localização atual.

Primeiro a maneira imperativa (em pseudocódigo)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

Agora a maneira funcional (em pseudocódigo). Estou apoiando-me fortemente no operador ternário, porque quero que pessoas de origens imperativas possam realmente ler este código. Portanto, se você não usa muito o operador ternário (eu sempre o evitava nos meus dias imperativos), aqui está como ele funciona.

predicate ? if-true-expression : if-false-expression

Você pode encadear a expressão ternária colocando uma nova expressão ternária no lugar da expressão falsa

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

Então, com isso em mente, aqui está a versão funcional.

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

Este é um exemplo trivial. Se isso estivesse movendo as pessoas pelo mundo do jogo, seria necessário introduzir efeitos colaterais, como desenhar a posição atual do objeto na tela e introduzir um pouco de atraso em cada chamada, com base na rapidez com que o objeto se move. Mas você ainda não precisaria de um estado mutável.

  1. A lição é que as linguagens funcionais "mudam" de estado chamando a função com parâmetros diferentes. Obviamente, isso realmente não modifica nenhuma variável, mas é assim que você obtém um efeito semelhante. Isso significa que você terá que se acostumar a pensar recursivamente, se quiser fazer uma programação funcional.

  2. Aprender a pensar recursivamente não é difícil, mas é preciso prática e um kit de ferramentas. A pequena seção do livro "Learn Java", onde eles usaram a recursão para calcular o fatorial, não é suficiente. Você precisa de um conjunto de habilidades como tornar processos iterativos recursivos (é por isso que a recursão final é essencial para a linguagem funcional), continuações, invariantes etc. Você não faria programação OO sem aprender sobre modificadores de acesso, interfaces etc. para programação funcional.

Minha recomendação é fazer o Little Schemer (observe que eu digo "faça" e não "leia") e depois faça todos os exercícios no SICP. Quando terminar, você terá um cérebro diferente do que quando começou.


8

Na verdade, é muito fácil ter algo que se parece com estado mutável, mesmo em idiomas sem estado mutável.

Considere uma função com o tipo s -> (a, s). Traduzindo da sintaxe Haskell, significa uma função que pega um parâmetro do tipo " s" e retorna um par de valores, dos tipos " a" e " s". Se sé o tipo de nosso estado, essa função pega um estado e retorna um novo estado e, possivelmente, um valor (você sempre pode retornar "unidade" aka (), que é equivalente a " void" em C / C ++, como o "a " tipo). Se você encadear várias chamadas de funções com tipos como este (retornando o estado de uma função e passando para a próxima), você terá um estado "mutável" (na verdade, você está em cada função criando um novo estado e abandonando o antigo )

Pode ser mais fácil entender se você imaginar o estado mutável como o "espaço" em que seu programa está sendo executado e depois pensar na dimensão do tempo. No instante t1, o "espaço" está em uma determinada condição (digamos, por exemplo, que algum local da memória tenha valor 5). Em um instante posterior t2, ele está em uma condição diferente (por exemplo, o local da memória agora possui o valor 10). Cada uma dessas "fatias" de tempo é um estado e é imutável (você não pode voltar no tempo para alterá-las). Portanto, desse ponto de vista, você passou do espaço-tempo completo com uma seta de tempo (seu estado mutável) para um conjunto de fatias de tempo-espaço (vários estados imutáveis), e seu programa está apenas tratando cada fatia como um valor e computando cada deles como uma função aplicada à anterior.

OK, talvez não tenha sido mais fácil de entender :-)

Pode parecer ineficiente representar explicitamente todo o estado do programa como um valor, que deve ser criado apenas para ser descartado no próximo instante (logo após a criação de um novo). Para alguns algoritmos, pode ser natural, mas quando não é, existe outro truque. Em vez de um estado real, você pode usar um estado falso que nada mais é do que um marcador (vamos chamar o tipo desse estado falso State#). Esse estado falso existe do ponto de vista da linguagem e é passado como qualquer outro valor, mas o compilador o omite completamente ao gerar o código da máquina. Serve apenas para marcar a sequência de execução.

Como exemplo, suponha que o compilador nos dê as seguintes funções:

readRef :: Ref a -> State# -> (a, State#)
writeRef :: Ref a -> a -> State# -> (a, State#)

A tradução dessas declarações do tipo Haskell readRefrecebe algo que se assemelha a um ponteiro ou um identificador para um valor do tipo " a" e o estado falso, e retorna o valor do tipo " a" apontado pelo primeiro parâmetro e um novo estado falso. writeRefé semelhante, mas altera o valor apontado.

Se você chamar readRefe depois passar o estado falso retornado por writeRef(talvez com outras chamadas para funções não relacionadas no meio; esses valores de estado criam uma "cadeia" de chamadas de função), ele retornará o valor gravado. Você pode chamar writeRefnovamente com o mesmo ponteiro / identificador e ele gravará no mesmo local da memória - mas, como conceitualmente está retornando um novo estado (falso), o estado (falso) ainda é imutável (um novo foi "criado "). O compilador chamará as funções na ordem em que teria que chamá-las se houvesse uma variável de estado real que tivesse que ser calculada, mas o único estado que existe é o estado completo (mutável) do hardware real.

(Aqueles que conhecem Haskell vai notar eu simplifiquei as coisas muito e omitido vários detalhes importantes. Para aqueles que querem ver mais detalhes, dê uma olhada Control.Monad.Statea partir da mtl, e ao ST se IOaka ( ST RealWorld) mônadas.)

Você pode se perguntar por que fazê-lo de maneira tão indireta (em vez de simplesmente ter um estado mutável no idioma). A vantagem real é que você reificou o estado do seu programa. O que antes estava implícito (o estado do seu programa era global, permitindo ações como a distância ) agora é explícito. Funções que não recebem e retornam o estado não podem modificá-lo ou ser influenciado por ele; eles são "puros". Melhor ainda, você pode ter threads de estado separados e, com um pouco de magia do tipo, eles podem ser usados ​​para incorporar uma computação imperativa em uma pura, sem torná-la impura (a STmônada em Haskell é a normalmente usada para esse truque; o que State#eu mencionei acima é de fato o GHC State# s, usado por sua implementação do STeIO mônadas).


7

A programação funcional evita o estado e enfatizafuncionalidade. Nunca existe um estado, embora o estado possa realmente ser algo imutável ou incorporado à arquitetura do que você está trabalhando. Considere a diferença entre um servidor Web estático que apenas carrega arquivos do sistema de arquivos e um programa que implementa o cubo de Rubik. O primeiro será implementado em termos de funções projetadas para transformar uma solicitação em uma solicitação de caminho do arquivo em uma resposta do conteúdo desse arquivo. Praticamente nenhum estado é necessário além de um pouquinho de configuração (o 'estado' do sistema de arquivos está realmente fora do escopo do programa. O programa funciona da mesma maneira, independentemente do estado em que os arquivos estão). No entanto, no último, você precisa modelar o cubo e a implementação do seu programa de como as operações nesse cubo alteram seu estado.


Quando eu era mais anti-funcional, eu me perguntava como poderia ser bom quando algo como um disco rígido é mutável. Todas as minhas classes de c # tinham um estado mutável e podiam simular muito logicamente um disco rígido ou qualquer outro dispositivo. Considerando que, com o funcional, houve uma incompatibilidade entre os modelos e as máquinas reais que eles estavam modelando. Depois de me aprofundar no funcionamento, percebi que os benefícios que você obtém são superiores ao problema. E se fosse fisicamente possível inventar um disco rígido que fizesse uma cópia de si mesmo, seria realmente útil (como o diário já faz).
LegendLength

5

Além das ótimas respostas que outras pessoas estão dando, pense nas aulas Integere Stringem Java. Instâncias dessas classes são imutáveis, mas isso não as torna inúteis apenas porque suas instâncias não podem ser alteradas. A imutabilidade oferece segurança. Você sabe se você usa uma instância String ou Inteiro como a chave para a Map, a chave não pode ser alterada. Compare isso com a Dateclasse em Java:

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

Você mudou silenciosamente uma chave no seu mapa! Trabalhar com objetos imutáveis, como na Programação Funcional, é muito mais limpo. É mais fácil argumentar sobre quais efeitos colaterais ocorrem - nenhum! Isso significa que é mais fácil para o programador e também para o otimizador.


2
Eu entendo isso, mas isso não responde à minha pergunta. Tendo em mente que um programa de computador é um modelo de algum evento ou processo do mundo real, se você não pode alterar seus valores, então como você modela algo que muda?
Mason Wheeler

Bem, certamente você pode fazer coisas úteis com as classes Integer e String. Não é como a imutabilidade deles significa que você não pode ter um estado mutável.
Eddie

@ Wheeler Mason - Ao entender que uma coisa e seu estado são duas "coisas" diferentes. O que é pacman não muda do tempo A para o tempo B. Onde está o pacman muda. Quando você passa do tempo A para o tempo B, obtém uma nova combinação de pacman + state ... que é o mesmo pacman, estado diferente. Estado não alterado ... estado diferente.
RHSeeger 02/07/2009

4

Para aplicativos altamente interativos, como jogos, a Programação Reativa Funcional é sua amiga: se você pode formular as propriedades do mundo do seu jogo como valores variáveis no tempo (e / ou fluxos de eventos), você está pronto! Essas fórmulas às vezes são ainda mais naturais e reveladoras de intenção do que alterar um estado; por exemplo, para uma bola em movimento, você pode usar diretamente a conhecida lei x = v * t . E o que é melhor, as regras do jogo escritas dessa maneira compõem melhor do que abstrações orientadas a objetos. Por exemplo, nesse caso, a velocidade da bola também pode ser um valor variável no tempo, que depende do fluxo de eventos que consiste nas colisões da bola. Para considerações de design mais concretas, consulte Criando jogos no Elm .


4

3

É assim que o FORTRAN funcionaria sem os blocos COMMON: você escreveria métodos com os valores passados ​​e variáveis ​​locais. É isso aí.

A programação orientada a objetos nos uniu estado e comportamento, mas era uma idéia nova quando o encontrei pela primeira vez em C ++, em 1994.

Nossa, eu era um programador funcional quando era engenheiro mecânico e não sabia disso!


2
Eu discordo que isso é algo que você pode fixar no OO. Os idiomas anteriores ao OO incentivavam o estado de acoplamento e algoritmos. OO apenas forneceu uma maneira melhor de gerenciá-lo.
Jason Baker

"Incentivado" - talvez. OO torna uma parte explícita da linguagem. Você pode encapsular e ocultar informações em C, mas eu diria que as linguagens OO tornam muito mais fácil.
duffymo

2

Lembre-se: as linguagens funcionais são Turing completas. Portanto, qualquer tarefa útil que você realizasse em uma linguagem imperativa pode ser realizada em uma linguagem funcional. No final do dia, porém, acho que há algo a ser dito sobre uma abordagem híbrida. Idiomas como F # e Clojure (e tenho certeza que outros) incentivam o design sem estado, mas permitem mutabilidade quando necessário.


Só porque duas línguas estão completas em Turing, não significa que elas possam executar as mesmas tarefas. O que isso significa é que eles podem executar o mesmo cálculo. Brainfuck é Turing completo, mas tenho certeza de que não pode se comunicar através de uma pilha TCP.
RHSeeger 02/07/2009

2
Claro que pode. Dado o mesmo acesso ao hardware que digamos C, ele pode. Isso não significa que seria prático, mas a possibilidade existe.
21430 Jason Baker

2

Você não pode ter uma linguagem funcional pura que seja útil. Sempre haverá um nível de mutabilidade com o qual você precisará lidar; o IO é um exemplo.

Pense nas linguagens funcionais como apenas mais uma ferramenta que você usa. É bom para certas coisas, mas não para outras. O exemplo do jogo que você deu pode não ser a melhor maneira de usar uma linguagem funcional, pelo menos a tela terá um estado mutável que você não pode fazer nada com o FP. A maneira como você pensa no problema e o tipo de problema que você resolve com o FP será diferente daqueles com os quais você está acostumado com a programação imperativa.



-3

Isto é muito simples. Você pode usar quantas variáveis ​​desejar na programação funcional ... mas apenas se forem variáveis locais (contidas em funções). Então, apenas envolva seu código em funções, passe valores entre essas funções (como parâmetros passados ​​e valores retornados) ... e isso é tudo!

Aqui está um exemplo:

function ReadDataFromKeyboard() {
    $input_values = $_POST[];
    return $input_values;
}
function ProcessInformation($input_values) {
    if ($input_values['a'] > 10)
        return ($input_values['a'] + $input_values['b'] + 3);
    else if ($input_values['a'] > 5)
        return ($input_values['b'] * 3);
    else
        return ($input_values['b'] - $input_values['a'] - 7);
}
function DisplayToPage($data) {
    print "Based your input, the answer is: ";
    print $data;
    print "\n";
}

/* begin: */
DisplayToPage (
    ProcessInformation (
        GetDataFromKeyboard()
    )
);

John, que idioma é esse?
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.