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?
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).