Ao programar no estilo Funcional, você tem um único estado de aplicativo que tece através da lógica do aplicativo?


12

Como eu construo um sistema que possui todos os seguintes itens :

  1. Usando funções puras com objetos imutáveis.
  2. 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)
  3. Evite ter muitos argumentos para funções.
  4. 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.


1
Não sou especialista em design e especialmente funcional, mas como seu jogo por natureza tem um estado que evolui, você tem certeza de que a programação funcional é um paradigma que se encaixa em todas as camadas do seu aplicativo?
Walfrat 05/07

Walfrat, acho que se você conversar com especialistas em programação funcional, provavelmente descobrirá que eles diriam que o paradigma de programação funcional tem soluções para gerenciar o estado em evolução.
Daisha Lynn

Sua pergunta me pareceu mais ampla que apenas afirma. Se é apenas sobre os estados de gestão aqui é um começo: ver a resposta e link dentro stackoverflow.com/questions/1020653/...
Walfrat

2
@DaishaLynn Não acho que você deva excluir a pergunta. Ele foi votado e ninguém está tentando fechá-lo, então não acho que esteja fora do escopo deste site. A falta de resposta até agora pode ser justamente porque requer alguma experiência relativamente específica. Mas isso não significa que não será encontrado e respondido eventualmente.
Ben Aaronson

2
Gerenciar o estado mutável em um programa funcional puro e complexo sem assistência substancial ao idioma é uma grande dor. Em Haskell, é gerenciável por causa de mônadas, sintaxe concisa, inferência de tipo muito boa, mas ainda pode ser muito irritante. Em C #, acho que você teria consideravelmente mais problemas.
Reintegrar Monica

Respostas:


2

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:

  • Divida a GameStatehierarquia, para obter 3-5 partes menores em vez de 15.
  • Deixe-o implementar interfaces, para que seus métodos vejam apenas as partes necessárias. Nunca os jogue de volta, pois você mentiria para si mesmo sobre o tipo real.
  • Deixe também que as partes implementem interfaces, para que você obtenha um controle preciso do que passa.
  • Use objetos de parâmetro, mas faça-o com moderação e tente transformá-los em objetos reais com seu próprio comportamento.
  • Às vezes, passar um pouco mais do que o necessário é melhor do que uma longa lista de parâmetros.

Então agora estou me perguntando se o problema que tenho é que minhas funções estão aninhadas muito profundamente.

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?


Alguém me disse para mudar meu design para que a função ever use apenas um parâmetro para que eu possa usar o curry. Eu tentei essa função, então, em vez de chamar DeleteEntity (a, b, c), agora chamo DeleteEntity (a) (b) (c). Então isso é fofo e deve tornar as coisas mais compostas, mas eu ainda não estou conseguindo.
21339 Daisha Lynn

@DaishaLynn Estou usando Java e não há açúcar sintático para curry, então (para mim) não vale a pena tentar. Sou bastante cético em relação ao possível uso de funções de ordem superior em nosso caso, mas deixe-me saber se funcionou para você.
Maaartinus 9/07

2

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

public class MyState
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
}

você pode definir interfaces IHasMyInte IHasMyStringcom métodos GetMyInte GetMyStringrespectivamente. A classe state se parece com:

public class MyState : IHasMyInt, IHasMyString
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
    public double MyDouble {get; set; }

    public int GetMyInt () 
    {
        return MyInt;
    }

    public string GetMyString ()
    {
        return MyString;
    }

    public double GetMyDouble ()
    {
        return MyDouble;
    }
}

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.

public static T DoSomething<T>(T state) where T : IHasMyString, IHasMyInt
{
    var s = state.GetMyString();
    var i = state.GetMyInt();
    return state;
}

Isso é interessante. Atualmente, onde passo a chamada de uma função e passo 10 parâmetros por valor, passo o "gameSt'ate" 10 vezes, mas para 10 tipos de parâmetros diferentes, como "IHasGameScore", "IHasGameBoard" etc. etc. foi uma maneira de passar passar um único parâmetro que a função pode indicar tem que implementar todas as interfaces nesse tipo. Gostaria de saber se posso fazê-lo com uma "restrição genérica" ​​.. Deixe-me tentar isso.
21917 Daisha Lynn

1
Funcionou. Aqui está funcionando: dotnetfiddle.net/cfmDbs .
21339 Daisha Lynn

1

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.


Não conheço Elm, mas acredito que seja semelhante ao Redux. No Redux, todos os redutores não são chamados para todas as mudanças de estado? Parece extremamente ineficiente.
Daisha Lynn

Quando se trata de otimizações de baixo nível, não assuma, meça. Na prática, é bastante rápido o suficiente.
Daniel T.

Obrigado Daniel, mas não vai funcionar para mim. Eu desenvolvi o suficiente para saber que ele não notifica todos os componentes da interface do usuário a qualquer momento em que os dados são alterados, independentemente de o controle se importar com o controle.
Daisha Lynn

-2

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.


3
Não emocionado com esta resposta, nem o tom. Não estou abusando de nada. Estou pressionando os limites do C # para usá-lo como uma linguagem mais funcional. Não é uma coisa incomum de se fazer. Você parece ser filosoficamente contrário a isso, o que é bom, mas nesse caso, não olhe para esta pergunta. Seu comentário não serve para ninguém. Ir em frente.
Daisha Lynn

@DaishaLynn você está errado, eu não sou contra isso de fato e eu o uso muito ... mas onde é natural e possível, e não tentando transformar uma linguagem OO em funcional, apenas porque ela é moderna para fazer isso. Você não precisa concordar com a minha resposta, mas isso não altera o fato de que você não está usando corretamente suas ferramentas.
t3chb0t

Eu não estou fazendo isso porque é moderno fazê-lo. O próprio C # está avançando no uso de um estilo funcional. O próprio Anders Hejlsberg indicou como tal. Entendo que você está interessado apenas no uso do idioma no fluxo principal e entendo por que e quando é apropriado. Só não sei por que alguém como você está nesse tópico. Como você está realmente ajudando?
Daisha Lynn

@DaishaLynn se você não consegue lidar com respostas que criticam sua pergunta ou abordagem, provavelmente não deve fazer perguntas aqui ou da próxima vez basta adicionar um aviso dizendo que está interessado apenas em respostas que apoiam sua ideia 100%, porque você não quer ouvir a verdade, mas obter opiniões favoráveis.
precisa saber é o seguinte

Por favor, sejam um pouco mais cordiais um com o outro. É possível fazer críticas sem menosprezar a linguagem. Tentar programar o C # em um estilo funcional certamente não é um "abuso" ou um caso extremo. É uma técnica comum usada por muitos desenvolvedores de C # para aprender com outros idiomas.
Zumalifeguard
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.