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.
fonte
Respostas:
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 vê " - 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:
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:
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:
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.
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:
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 #:
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.
fonte
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:
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:
mas eu também poderia fazer
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.
fonte
int double(x){ return x * (++y); }
desde o atual ainda será 4, apesar de ainda ter um efeito colateral não divulgada, enquanto que++y
voltará 6.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.
fonte
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:
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:
Coloque parênteses para esclarecer o que isso significa:
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.
fonte
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:
torna-se este código funcional (sintaxe semelhante ao esquema):
ou este código haskellish
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 .
fonte
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.
fonte
Estou atrasado para a discussão, mas queria acrescentar alguns pontos para as pessoas que estão lutando com a programação funcional.
Primeiro a maneira imperativa (em pseudocódigo)
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.
Você pode encadear a expressão ternária colocando uma nova expressão ternária no lugar da expressão falsa
Então, com isso em mente, aqui está a versão funcional.
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.
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.
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.
fonte
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
". Ses
é 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:
A tradução dessas declarações do tipo Haskell
readRef
recebe 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
readRef
e depois passar o estado falso retornado porwriteRef
(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 chamarwriteRef
novamente 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.State
a partir damtl
, e aoST s
eIO
aka (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
ST
mônada em Haskell é a normalmente usada para esse truque; o queState#
eu mencionei acima é de fato o GHCState# s
, usado por sua implementação doST
eIO
mônadas).fonte
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.
fonte
Além das ótimas respostas que outras pessoas estão dando, pense nas aulas
Integer
eString
em 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 aMap
, a chave não pode ser alterada. Compare isso com aDate
classe em Java: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.
fonte
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 .
fonte
Usando alguma criatividade e correspondência de padrões, foram criados jogos sem estado:
bem como demonstrações de rolamento:
e visualizações:
fonte
É 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!
fonte
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.
fonte
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.
fonte
Usando muita recursão.
Tic Tac Toe em F # (uma linguagem funcional.)
fonte
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:
fonte