Estou lendo e ouvindo que as pessoas (também neste site) elogiam rotineiramente o paradigma de programação funcional, enfatizando como é bom ter tudo imutável. Notavelmente, as pessoas propõem essa abordagem mesmo em linguagens OO tradicionalmente imperativas, como C #, Java ou C ++, não apenas em linguagens puramente funcionais como Haskell, que impõem isso ao programador.
Acho difícil entender, porque acho a mutabilidade e os efeitos colaterais ... convenientes. No entanto, dada a forma como as pessoas atualmente condenam os efeitos colaterais e consideram uma boa prática livrar-se deles sempre que possível, acredito que, se quiser ser um programador competente, tenho que iniciar minha jornada para uma melhor compreensão do paradigma ... Daí meu Q.
Um lugar quando encontro problemas com o paradigma funcional é quando um objeto é naturalmente referenciado de vários lugares. Deixe-me descrevê-lo em dois exemplos.
O primeiro exemplo será o meu jogo de C # que estou tentando criar no meu tempo livre . É um jogo na web baseado em turnos, onde ambos os jogadores têm equipes de 4 monstros e podem enviar um monstro do seu time para o campo de batalha, onde enfrentará o monstro enviado pelo jogador adversário. Os jogadores também podem recuperar monstros do campo de batalha e substituí-los por outro monstro de seu time (da mesma forma que Pokemon).
Nesse cenário, um único monstro pode ser referenciado naturalmente de pelo menos 2 lugares: o time de um jogador e o campo de batalha, que referencia dois monstros "ativos".
Agora vamos considerar a situação em que um monstro é atingido e perde 20 pontos de vida. Entre os parênteses do paradigma imperativo, modifico o health
campo desse monstro para refletir essa mudança - e é isso que estou fazendo agora. No entanto, isso torna a Monster
classe mutável e as funções (métodos) relacionadas impuras, o que eu acho que é considerado uma prática ruim a partir de agora.
Mesmo que eu tenha me dado a permissão para ter o código deste jogo em um estado abaixo do ideal para ter alguma esperança de realmente terminá-lo em algum momento do futuro, gostaria de saber e entender como deve ser. escrito corretamente. Portanto: se essa é uma falha de design, como corrigi-la?
No estilo funcional, como eu o entendo, eu faria uma cópia desse Monster
objeto, mantendo-o idêntico ao antigo, exceto nesse campo; e o método suffer_hit
retornaria esse novo objeto em vez de modificar o antigo. Da mesma forma, eu copiava o Battlefield
objeto, mantendo todos os seus campos iguais, exceto esse monstro.
Isso vem com pelo menos 2 dificuldades:
- A hierarquia pode facilmente ser muito mais profunda do que este exemplo simplificado de apenas
Battlefield
->Monster
. Eu teria que fazer essa cópia de todos os campos, exceto um, e retornar um novo objeto por toda essa hierarquia. Este seria um código padrão que acho irritante, especialmente porque a programação funcional deve reduzir o padrão. - Um problema muito mais grave, no entanto, é que isso levaria os dados a ficarem fora de sincronia . O monstro ativo do campo teria sua saúde reduzida; no entanto, esse mesmo monstro, referenciado pelo seu jogador controlador
Team
, não o faria. Se eu adotasse o estilo imperativo, todas as modificações de dados seriam instantaneamente visíveis de todos os outros locais de código e, em casos como esse, acho realmente conveniente - mas a maneira como estou obtendo as coisas é exatamente o que as pessoas dizem que é errado com o estilo imperativo!- Agora seria possível resolver esse problema fazendo uma jornada até o
Team
final de cada ataque. Isso é trabalho extra. No entanto, e se um monstro puder ser repentinamente referenciado posteriormente de mais lugares? E se eu tiver uma habilidade que, por exemplo, permita que um monstro se concentre em outro monstro que não esteja necessariamente em campo (na verdade, estou considerando essa habilidade)? Eu certamente me lembrarei de fazer também uma jornada para monstros focados imediatamente após cada ataque? Parece ser uma bomba-relógio que explodirá à medida que o código se tornar mais complexo, então acho que não há solução.
- Agora seria possível resolver esse problema fazendo uma jornada até o
Uma ideia para uma solução melhor vem do meu segundo exemplo, quando acertei o mesmo problema. Na academia, fomos instruídos a escrever em Haskell um intérprete de uma linguagem de nosso próprio projeto. (Foi também assim que fui forçado a começar a entender o que é FP). O problema apareceu quando eu estava implementando fechamentos. Mais uma vez, o mesmo escopo agora pode ser referenciado de vários locais: através da variável que contém esse escopo e como o escopo pai de qualquer escopo aninhado! Obviamente, se uma alteração for feita nesse escopo por meio de qualquer uma das referências que apontam para ele, essa alteração também deverá ser visível em todas as outras referências.
A solução que eu vim foi atribuir um ID a cada escopo e manter um dicionário central de todos os escopos na State
mônada. Agora, as variáveis reteriam apenas o ID do escopo ao qual estavam vinculados, e não o próprio escopo, e os escopos aninhados também manteriam o ID do escopo pai.
Eu acho que a mesma abordagem poderia ser tentada no meu jogo de luta contra monstros ... Campos e equipes não fazem referência a monstros; eles possuem IDs de monstros salvos em um dicionário central de monstros.
No entanto, mais uma vez, vejo um problema com essa abordagem que me impede de aceitá-la sem hesitar como a solução para o problema:
Mais uma vez, é uma fonte de código padrão. Torna uma linha necessariamente três linhas: o que anteriormente era uma modificação local de uma linha de um único campo agora requer (a) Recuperação do objeto do dicionário central (b) Realização da alteração (c) Salvamento do novo objeto para o dicionário central. Além disso, manter identificações de objetos e dicionários centrais em vez de ter referências aumenta a complexidade. Como o FP é anunciado para reduzir a complexidade e o código padrão, isso sugere que estou fazendo errado.
Eu também escreveria sobre um segundo problema que parece muito mais grave: essa abordagem introduz vazamentos de memória . Objetos inacessíveis normalmente serão coletados como lixo. No entanto, objetos mantidos em um dicionário central não podem ser coletados como lixo, mesmo que nenhum objeto acessível faça referência a esse ID específico. E, embora uma programação teoricamente cuidadosa possa evitar vazamentos de memória (poderíamos ter o cuidado de remover manualmente cada objeto do dicionário central quando não for mais necessário), isso é propenso a erros e o FP é anunciado para aumentar a correção dos programas. não ser o caminho correto.
No entanto, descobri a tempo que parece ser um problema resolvido. O Java fornece WeakHashMap
que podem ser usados para resolver esse problema. O C # fornece um recurso semelhante - ConditionalWeakTable
- embora, de acordo com os documentos, ele deva ser usado pelos compiladores. E em Haskell, temos System.Mem.Weak .
Armazenar esses dicionários é a solução funcional correta para esse problema ou existe uma mais simples que não consigo ver? Imagino que o número de dicionários desse tipo possa crescer facilmente e muito; Portanto, se essas dicção também devem ser imutáveis, isso pode significar muita passagem de parâmetros ou, em linguagens que suportam isso, cálculos monádicos, já que os dicionários seriam mantidos em mônadas (mas mais uma vez eu estou lendo isso em funções puramente funcionais). idiomas, o mínimo de código possível deve ser monádico, enquanto essa solução de dicionário colocaria quase todo o código dentro da State
mônada; o que mais uma vez me faz duvidar se essa é a solução correta.)
Após algumas considerações, acho que acrescentaria mais uma pergunta: o que estamos ganhando com a construção desses dicionários? O que há de errado com a programação imperativa é, de acordo com muitos especialistas, que as mudanças em alguns objetos se propagam para outras partes do código. Para resolver esse problema, os objetos devem ser imutáveis - precisamente por esse motivo, se bem entendi, as alterações feitas a eles não devem ser visíveis em nenhum outro lugar. Mas agora estou preocupado com outros trechos de código operando com dados desatualizados, então inventei dicionários centrais para que ... mais uma vez as alterações em alguns trechos de código se propagem para outros trechos de código! Não voltamos, portanto, ao estilo imperativo, com todas as suas supostas desvantagens, mas com maior complexidade?
fonte
Team
) podem recuperar o resultado da batalha e, portanto, os estados dos monstros por uma tupla (número da batalha, ID da entidade do monstro).Respostas:
Como a Programação Funcional lida com um objeto referenciado de vários lugares? Ele convida você a revisitar seu modelo!
Para explicar ... vejamos como os jogos em rede às vezes são gravados - com uma cópia "fonte dourada" central do estado do jogo e um conjunto de eventos de cliente que atualizam esse estado e depois são transmitidos de volta para os outros clientes .
Você pode ler sobre a diversão que a equipe do Factorio teve ao fazê-lo se comportar bem em algumas situações; Aqui está uma breve visão geral de seu modelo:
O principal é que o estado de cada objeto seja imutável na marca específica na linha do tempo . Tudo no estado multiplayer global deve finalmente convergir para uma realidade determinística.
E - essa pode ser a chave da sua pergunta. O estado de cada entidade é imutável para um determinado tique, e você acompanha os eventos de transição que produzem novas instâncias ao longo do tempo.
Se você pensar bem, a fila de eventos recebidos do servidor deve ter acesso a um diretório central de entidades, apenas para que possa aplicar seus eventos.
No final, seus métodos simples de mutação de uma linha que você não deseja complicar são apenas simples porque você não está realmente modelando o tempo com precisão. Afinal, se a integridade puder mudar no meio do ciclo de processamento, as entidades anteriores desse tick verão um valor antigo e as posteriores, um valor alterado. Gerenciar isso com cuidado significa, no mínimo , os estados atual (imutável) e o próximo (em construção), que são realmente apenas dois ticks na grande linha do tempo dos ticks!
Portanto, como um guia amplo, considere dividir o estado de um monstro em vários objetos pequenos relacionados a, por exemplo, localização / velocidade / física, saúde / dano, ativos. Construa um evento para descrever cada mutação que possa acontecer e execute seu loop principal como:
Ou algo assim. Acho que penso "como eu faria isso distribuído?" é um bom exercício mental, geralmente, para refinar minha compreensão quando estou confuso sobre onde as coisas vivem e como elas devem evoluir.
Graças a uma observação de @ AaronM.Eshbach, destacando que este é um domínio de problema semelhante ao Event Sourcing e ao padrão CQRS , onde você está modelando alterações de estado em um sistema distribuído como uma série de eventos imutáveis ao longo do tempo . Nesse caso, provavelmente estamos tentando limpar um aplicativo de banco de dados complexo, segregando (como o nome sugere!) O tratamento do comando mutator do sistema de consulta / exibição. Mais complexo, é claro, mas mais flexível.
fonte
Você ainda está na metade do campo imperativo. Em vez de pensar em um único objeto de cada vez, pense no seu jogo em termos de um histórico de jogadas ou eventos
etc
Você pode calcular o estado do jogo a qualquer momento, encadeando as ações para produzir um objeto de estado imutável. Cada peça é uma função que pega um objeto de estado e retorna um novo objeto de estado
fonte