Mantendo o estado complexo em Haskell

Suponha que você esteja construindo uma simulação bastante grande em Haskell. Existem muitos tipos diferentes de entidades cujos atributos são atualizados à medida que a simulação avança. Vamos dizer, por exemplo, que suas entidades são chamadas Macacos, Elefantes, Ursos, etc.

Qual é o seu método preferido para manter os estados dessas entidades?

A primeira e mais óbvia abordagem que pensei foi esta:

mainLoop :: [Monkey] -> [Elephant] -> [Bear] -> String
mainLoop monkeys elephants bears =
  let monkeys'   = updateMonkeys   monkeys
      elephants' = updateElephants elephants
      bears'     = updateBears     bears
  in
    if shouldExit monkeys elephants bears then "Done" else
      mainLoop monkeys' elephants' bears'

Já é feio ter cada tipo de entidade explicitamente mencionado nomainLoop assinatura de função. Você pode imaginar como seria absolutamente horrível se tivesse, digamos, 20 tipos de entidades. (20 não é desarrazoado para simulações complexas.) Então eu acho que essa é uma abordagem inaceitável. Mas sua graça salvadora é que funciona comoupdateMonkeys são muito explícitos no que fazem: eles pegam uma lista de Macacos e retornam um novo.

Então, o próximo pensamento seria colocar tudo em uma grande estrutura de dados que mantenha todo o estado, limpando assim a assinatura demainLoop:

mainLoop :: GameState -> String
mainLoop gs0 =
  let gs1 = updateMonkeys   gs0
      gs2 = updateElephants gs1
      gs3 = updateBears     gs2
  in
    if shouldExit gs0 then "Done" else
      mainLoop gs3

Alguns sugerem que nós embrulhamosGameState em uma Monad State e ligueupdateMonkeys etc em umdo. Isso é bom. Alguns preferem sugerir que limpemos a composição da função. Também bem, eu acho. (BTW, eu sou um novato com Haskell, então talvez eu esteja errado sobre isso.)

Mas então o problema é, funções comoupdateMonkeys não forneça informações úteis da assinatura do tipo. Você não pode ter certeza do que eles fazem. Certo,updateMonkeys é um nome descritivo, mas isso é pouco consolo. Quando eu passo em umobjeto deus e diga "por favor, atualize meu estado global", sinto que estamos de volta ao mundo imperativo. Parece variáveis ​​globais por outro nome: você tem uma função que fazalguma coisa para o estado global, você o chama e espera pelo melhor. (Suponho que você ainda evite alguns problemas de simultaneidade que estariam presentes com variáveis ​​globais em um programa imperativo. Mas meh, a simultaneidade não é quase a única coisa errada com variáveis ​​globais.)

Um outro problema é o seguinte: suponha que os objetos precisem interagir. Por exemplo, temos uma função como esta:

stomp :: Elephant -> Monkey -> (Elephant, Monkey)
stomp elephant monkey =
  (elongateEvilGrin elephant, decrementHealth monkey)

Diga isso é chamadoupdateElephants, porque é lá que nós verificamos se algum dos elefantes está no alcance de qualquer macaco. Como você elegantemente propaga as mudanças para os macacos e elefantes neste cenário? Em nosso segundo exemplo,updateElephants pega e retorna um objeto deus, então isso pode afetar ambas as mudanças. Mas isso apenas turva as águas e reforça meu ponto: com o objeto deus, você está efetivamente apenas mudando as variáveis ​​globais. E se você não estiver usando o objeto deus, não sei como propagaria esses tipos de alterações.

O que fazer? Certamente muitos programas precisam gerenciar estados complexos, então acredito que existam algumas abordagens bem conhecidas para esse problema.

Apenas por uma questão de comparação, eis como eu poderia resolver o problema no mundo da OOP. HaveriaMonkey, Elephant, etc. objetos. Eu provavelmente teria métodos de classe para fazer pesquisas no conjunto de todos os animais vivos. Talvez você possa pesquisar por localização, por ID, o que for. Graças às estruturas de dados subjacentes às funções de pesquisa, elas permaneceriam alocadas no heap. (Eu estou supondo GC ou contagem de referência.) Suas variáveis ​​de membro seriam mutadas o tempo todo. Qualquer método de qualquer classe seria capaz de transformar qualquer animal vivo de qualquer outra classe. Por exemplo. aElephant poderia ter umstomp método que diminuiria a saúde de um falecidoMonkey objeto, e não haveria necessidade de passar

Da mesma forma, em um Erlang ou outro design orientado a atores, você poderia resolver esses problemas com bastante elegância: cada ator mantém seu próprio loop e, portanto, seu próprio estado, de modo que você nunca precisa de um objeto deus. E a passagem de mensagens permite que as atividades de um objeto disparem alterações em outros objetos sem passar um monte de coisas por todo o caminho de volta até a pilha de chamadas. No entanto, ouvi dizer que os atores de Haskell são mal vistos.

questionAnswers(2)

yourAnswerToTheQuestion