Como eu construo um sistema que possui todos os seguintes itens :
- Usando funções puras com objetos imutáveis.
- Somente passe para os dados de uma função a função necessária, não mais (isto é, nenhum objeto grande de estado do aplicativo)
- Evite ter muitos argumentos para funções.
- Evite ter que construir novos objetos apenas para empacotar e descompactar parâmetros para funções, simplesmente para evitar que muitos parâmetros sejam passados para funções. Se eu vou empacotar vários itens em uma função como um único objeto, quero que esse objeto seja o proprietário desses dados, não algo construído temporariamente
Parece-me que a mônada do Estado infringe a regra nº 2, embora não seja óbvia porque é tecida através da mônada.
Tenho a sensação de que preciso usar as lentes de alguma forma, mas muito pouco está escrito sobre isso para idiomas não funcionais.
fundo
Como exercício, estou convertendo um dos meus aplicativos existentes de um estilo orientado a objetos para um estilo funcional. A primeira coisa que estou tentando fazer é criar o máximo possível do núcleo interno do aplicativo.
Uma coisa que ouvi é que como gerenciar o "Estado" em uma linguagem puramente funcional, e é isso que acredito ser feito pelas mônadas do Estado, é que, logicamente, você chama uma função pura ", passando pelo estado do mundo como está ", e quando a função retornar, ela retornará para você o estado do mundo conforme foi alterado.
Para ilustrar, a maneira como você pode fazer um "olá mundo" de uma maneira puramente funcional é como, você passa em seu programa esse estado da tela e recebe de volta o estado da tela com "olá mundo" impresso. Portanto, tecnicamente, você está fazendo uma chamada para uma função pura e não há efeitos colaterais.
Com base nisso, examinei meu aplicativo e: 1. Primeiro, coloquei todo o estado do meu aplicativo em um único objeto global (GameState). 2. Segundo, tornei o GameState imutável. Você não pode mudar isso. Se você precisar de uma alteração, precisará criar uma nova. Fiz isso adicionando um construtor de cópia, que opcionalmente utiliza um ou mais campos que foram alterados. 3. Para cada aplicativo, passo o GameState como parâmetro. Dentro da função, depois de fazer o que vai fazer, ele cria um novo GameState e o devolve.
Como eu tenho um núcleo funcional puro e um loop externo que alimenta esse GameState no loop principal do fluxo de trabalho do aplicativo.
Minha pergunta:
Agora, meu problema é que, o GameState possui cerca de 15 objetos imutáveis diferentes. Muitas das funções no nível mais baixo operam apenas em alguns desses objetos, como a pontuação. Então, digamos que eu tenho uma função que calcula a pontuação. Hoje, o GameState é passado para essa função, que modifica a pontuação criando um novo GameState com uma nova pontuação.
Algo sobre isso parece errado. A função não precisa da totalidade do GameState. Ele só precisa do objeto Score. Então eu atualizei para passar a Pontuação e retornar apenas a Pontuação.
Isso parecia fazer sentido, então fui além com outras funções. Algumas funções exigiriam que eu passasse 2, 3 ou 4 parâmetros do GameState, mas como usei o padrão por todo o núcleo externo do aplicativo, estou passando cada vez mais o estado do aplicativo. Como, no topo do loop do fluxo de trabalho, eu chamaria um método, que chamaria um método que chamaria um método etc., até o local onde a pontuação é calculada. Isso significa que a pontuação atual é transmitida por todas essas camadas apenas porque uma função na parte inferior calcula a pontuação.
Então agora eu tenho funções com algumas vezes dezenas de parâmetros. Eu poderia colocar esses parâmetros em um objeto para diminuir o número de parâmetros, mas gostaria que essa classe fosse o local principal do estado do aplicativo de estado, em vez de um objeto que é simplesmente construído no momento da chamada, para evitar passar em vários parâmetros e descompacte-os.
Então agora estou me perguntando se o problema que tenho é que minhas funções estão aninhadas muito profundamente. Esse é o resultado de querer ter funções pequenas, então refatoro quando uma função fica grande e a divido em várias funções menores. Mas fazer isso produz uma hierarquia mais profunda, e qualquer coisa passada para as funções internas precisa ser passada para a função externa, mesmo que a função externa não esteja operando diretamente nesses objetos.
Parecia que simplesmente passar no GameState ao longo do caminho evitava esse problema. Mas estou de volta ao problema original de passar mais informações para uma função do que a função precisa.
fonte
Respostas:
Não tenho certeza se existe uma boa solução. Isso pode ou não ser uma resposta, mas é muito longo para um comentário. Eu estava fazendo uma coisa semelhante e os seguintes truques ajudaram:
GameState
hierarquia, para obter 3-5 partes menores em vez de 15.Acho que não. Refatorar em pequenas funções é correto, mas talvez você possa reagrupá-las melhor. Às vezes, não é possível, às vezes, apenas precisa de um segundo (ou terceiro) olhar para o problema.
Compare seu design com o mutável. Existem coisas que pioraram com a reescrita? Em caso afirmativo, você não pode melhorá-los da mesma maneira que fez originalmente?
fonte
Não posso falar com c #, mas em Haskell, você acabaria passando todo o estado. Você pode fazer isso explicitamente ou com uma mônada do estado. Uma coisa que você pode fazer para resolver o problema das funções que recebem mais informações, então elas precisam é usar Tem classes de tipo. (Se você não estiver familiarizado, as classes de tipo Haskell são um pouco como as interfaces C #.) Para cada elemento E do estado, você pode definir um HasE de classe de tipo que requer uma função getE que retorna o valor de E. A mônada de estado pode ser fez uma instância de todas essas classes de tipo. Em suas funções reais, em vez de exigir explicitamente a mônada do seu estado, você precisa de qualquer mônada pertencente às classes de tipo Tem para os elementos necessários; que restringe o que a função pode fazer com a mônada que está usando. Para mais informações sobre essa abordagem, consulte Michael Snoyman.publicar no padrão de design do ReaderT .
Você provavelmente poderia replicar algo assim em C #, dependendo de como está definindo o estado que está sendo transmitido. Se você tem algo como
você pode definir interfaces
IHasMyInt
eIHasMyString
com métodosGetMyInt
eGetMyString
respectivamente. A classe state se parece com:seus métodos podem exigir IHasMyInt, IHasMyString ou todo o MyState, conforme apropriado.
Em seguida, você pode usar a restrição where na definição da função para poder passar o objeto state, mas ele pode chegar apenas a string e int, e não ao dobro.
fonte
Eu acho que você faria bem em aprender sobre Redux ou Elm e como eles lidam com essa questão.
Basicamente, você tem uma função pura que executa todo o estado e a ação que o usuário executou e retorna o novo estado.
Essa função chama outras funções puras, cada uma das quais lida com uma parte específica do estado. Dependendo da ação, muitas dessas funções podem fazer nada além de retornar o estado original inalterado.
Para saber mais, pesquise no Google the Elm Architecture ou no Redux.js.org.
fonte
Eu acho que o que você está tentando fazer é usar uma linguagem orientada a objetos de uma maneira que não deva ser usada, como se fosse uma funcional pura. Não é como se as línguas OO fossem todas más. Existem vantagens em qualquer uma das abordagens, e é por isso que agora podemos misturar estilo OO com estilo funcional e ter a oportunidade de tornar funcionais alguns códigos, enquanto outros permanecem orientados a objetos, para que possamos tirar proveito de toda a reutilização, herança ou polimofismo. Felizmente, não estamos mais vinculados a nenhuma das duas abordagens, apenas, por que você está tentando se limitar a uma delas?
Respondendo à sua pergunta: não, eu não tento nenhum estado específico através da lógica do aplicativo, mas uso o que é apropriado para o caso de uso atual e aplico as técnicas disponíveis da maneira mais apropriada.
C # ainda não está pronto para ser usado tão funcional quanto você gostaria.
fonte