Al programar en estilo funcional, ¿tiene un solo estado de aplicación que entreteje a través de la lógica de la aplicación?

12

¿Cómo construyo un sistema que tenga todo lo siguiente ?

  1. Usar funciones puras con objetos inmutables.
  2. Solo pase a los datos de una función que la función necesita, no más (es decir, ningún objeto de estado de aplicación grande)
  3. Evite tener demasiados argumentos para las funciones.
  4. Evite tener que construir nuevos objetos solo con el propósito de empaquetar y desempaquetar parámetros a funciones, simplemente para evitar que se pasen demasiados parámetros a funciones. Si voy a empacar varios elementos en una función como un solo objeto, quiero que ese objeto sea el propietario de esos datos, no algo construido temporalmente

Me parece que la mónada estatal rompe la regla # 2, aunque no es obvio porque está entretejida a través de la mónada.

Tengo la sensación de que necesito usar lentes de alguna manera, pero se escribe muy poco al respecto para los lenguajes no funcionales.

Antecedentes

Como ejercicio, estoy convirtiendo una de mis aplicaciones existentes de un estilo orientado a objetos a un estilo funcional. Lo primero que intento hacer es aprovechar al máximo el núcleo interno de la aplicación.

Una cosa que he escuchado es que cómo manejar el "Estado" en un lenguaje puramente funcional, y esto es lo que creo que hacen las mónadas estatales, es que lógicamente, ustedes llaman una función pura ", pasando el estado del mundo tal como es ", luego, cuando la función regresa, le devuelve el estado del mundo tal como ha cambiado.

Para ilustrar, la forma en que puede hacer un "hola mundo" de una manera puramente funcional es algo así como, pasa su programa en ese estado de la pantalla y recibe el estado de la pantalla con "hola mundo" impreso en él. Entonces, técnicamente, estás llamando a una función pura, y no hay efectos secundarios.

En base a eso, revisé mi aplicación y: 1. Primero puse todo el estado de mi aplicación en un solo objeto global (GameState) 2. En segundo lugar, hice que GameState fuera inmutable. No puedes cambiarlo. Si necesita un cambio, debe construir uno nuevo. Hice esto agregando un constructor de copia, que opcionalmente toma uno o más campos que cambiaron. 3. Para cada aplicación, paso el GameState como parámetro. Dentro de la función, después de hacer lo que va a hacer, crea un nuevo GameState y lo devuelve.

Cómo tengo un núcleo funcional puro y un bucle en el exterior que alimenta ese GameState en el bucle de flujo de trabajo principal de la aplicación.

Mi pregunta:

Ahora, mi problema es que, GameState tiene unos 15 objetos inmutables diferentes. Muchas de las funciones en el nivel más bajo solo operan en algunos de esos objetos, como mantener la puntuación. Entonces, digamos que tengo una función que calcula el puntaje. Hoy, GameState se pasa a esta función, que modifica el puntaje creando un nuevo GameState con un nuevo puntaje.

Algo sobre eso parece estar mal. La función no necesita la totalidad de GameState. Solo necesita el objeto Score. Así que lo actualicé para aprobar el puntaje y devolver solo el puntaje.

Eso parecía tener sentido, así que fui más allá con otras funciones. Algunas funciones requerirían que pase 2, 3 o 4 parámetros desde GameState, pero a medida que utilicé el patrón en todo el núcleo externo de la aplicación, paso cada vez más el estado de la aplicación. Como, en la parte superior del ciclo de flujo de trabajo, llamaría a un método, que llamaría a un método que llamaría a un método, etc., hasta el punto donde se calcula el puntaje. Eso significa que el puntaje actual se pasa a través de todas esas capas solo porque una función en la parte inferior va a calcular el puntaje.

Entonces ahora tengo funciones con a veces docenas de parámetros. Podría poner esos parámetros en un objeto para reducir el número de parámetros, pero luego me gustaría que esa clase sea la ubicación maestra del estado de la aplicación de estado, en lugar de un objeto que simplemente se construye en el momento de la llamada simplemente para evitar pasar en múltiples parámetros, y luego descomprimirlos.

Así que ahora me pregunto si el problema que tengo es que mis funciones están demasiado anidadas. Este es el resultado de querer tener funciones pequeñas, así que refactorizo ​​cuando una función llega a ser grande y la divido en múltiples funciones más pequeñas. Pero hacer eso produce una jerarquía más profunda, y todo lo que se pasa a las funciones internas debe pasar a la función externa, incluso si la función externa no está operando directamente en esos objetos.

Parecía que simplemente pasar GameState por el camino evitaba este problema. Pero vuelvo al problema original de pasar más información a una función que la función necesita.

Daisha Lynn
fuente
1
No soy un experto en diseño y especialmente funcional, pero dado que su juego por naturaleza tiene un estado que evoluciona, ¿está seguro de que la programación funcional es un paradigma que se adapta a todas las capas de su aplicación?
Walfrat
Walfrat, creo que si hablas con expertos en programación funcional, probablemente encontrarás que dirían que el paradigma de la programación funcional tiene soluciones para gestionar el estado en evolución.
Daisha Lynn
Su pregunta me pareció más amplia que solo afirma. Si solo se trata de administrar estados, este es un comienzo: vea la respuesta y el enlace en stackoverflow.com/questions/1020653/…
Walfrat
2
@DaishaLynn No creo que deba eliminar la pregunta. Ha sido votado y nadie está tratando de cerrarlo, así que no creo que esté fuera del alcance de este sitio. La falta de una respuesta hasta ahora puede ser solo porque requiere cierta experiencia relativamente específica. Pero eso no significa que no se encuentre y se responda eventualmente.
Ben Aaronson
2
Administrar el estado mutable en un programa funcional puro y complejo sin una asistencia sustancial de lenguaje es un gran dolor. En Haskell es manejable debido a mónadas, sintaxis concisa, muy buena inferencia de tipos, pero aún puede ser muy molesto. En C # creo que tendrías muchos más problemas.
Vuelva a instalar Mónica

Respuestas:

2

No estoy seguro de si hay una buena solución. Esto puede o no ser una respuesta, pero es demasiado largo para un comentario. Estaba haciendo algo similar y los siguientes trucos me han ayudado:

  • Divida la GameStatejerarquía, para obtener 3-5 partes más pequeñas en lugar de 15.
  • Deje que implemente interfaces, para que sus métodos solo vean las partes necesarias. Nunca los vuelvas a tirar, ya que te mentirías a ti mismo sobre el tipo real.
  • Deje también que las partes implementen interfaces, para que obtenga un control preciso de lo que pasa.
  • Use objetos de parámetros, pero hágalo con moderación e intente convertirlos en objetos reales con su propio comportamiento.
  • A veces, pasar un poco más de lo necesario es mejor que una larga lista de parámetros.

Así que ahora me pregunto si el problema que tengo es que mis funciones están demasiado anidadas.

No lo creo. Refactorizar en pequeñas funciones es correcto, pero tal vez podría reagruparlas mejor. A veces, no es posible, a veces solo necesita una segunda (o tercera) mirada al problema.

Compare su diseño con el mutable. ¿Hay cosas que han empeorado con la reescritura? Si es así, ¿no puedes mejorarlos de la misma manera que lo hiciste originalmente?

maaartinus
fuente
Alguien me dijo que cambiara mi diseño para que alguna vez la función tome solo un parámetro para que pueda usar curry. Intenté esa función, así que en lugar de llamar a DeleteEntity (a, b, c), ahora llamo a DeleteEntity (a) (b) (c). Así que eso es lindo, y se supone que hace que las cosas sean más composibles, pero todavía no lo entiendo.
Daisha Lynn
@DaishaLynn Estoy usando Java y no hay azúcar sintáctica dulce para curry, así que (para mí) no vale la pena intentarlo. Soy bastante escéptico sobre el posible uso de funciones de orden superior en nuestro caso, pero avíseme si funcionó para usted.
maaartinus
2

No puedo hablar con C #, pero en Haskell, terminarías pasando todo el estado. Puede hacer esto explícitamente o con una mónada estatal. Una cosa que puede hacer para abordar el problema de las funciones que reciben más información de la que necesitan es usar las clases tipográficas Has. (Si no está familiarizado, las clases de tipos de Haskell son un poco como las interfaces de C #). Para cada elemento E del estado, puede definir una clase de tipo HasE que requiera una función getE que devuelva el valor de E. La mónada de estado puede entonces ser hizo una instancia de todas estas clases de tipos. Luego, en sus funciones reales, en lugar de requerir explícitamente su mónada estatal, necesita cualquier mónada que pertenezca a las clases de tipo Has para los elementos que necesita; eso restringe lo que la función puede hacer con la mónada que está usando. Para obtener más información sobre este enfoque, consulte Michael Snoymanpublicar en el patrón de diseño ReaderT .

Probablemente podría replicar algo como esto en C #, dependiendo de cómo esté definiendo el estado que se está transmitiendo. Si tienes algo como

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

podría definir interfaces IHasMyInty IHasMyStringcon métodos GetMyInty GetMyStringrespectivamente. La clase de estado se ve así:

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;
    }
}

entonces sus métodos pueden requerir IHasMyInt, IHasMyString o todo MyState según corresponda.

Luego puede usar la restricción where en la definición de la función para poder pasar el objeto de estado, pero solo puede llegar a string e int, no al doble.

public static T DoSomething<T>(T state) where T : IHasMyString, IHasMyInt
{
    var s = state.GetMyString();
    var i = state.GetMyInt();
    return state;
}
DylanSp
fuente
Eso es interesante. Por lo tanto, actualmente, cuando paso llamar a una función y paso 10 parámetros por valor, paso el "gameSt'ate" 10 veces, pero a 10 tipos de parámetros diferentes, como "IHasGameScore", "IHasGameBoard", etc. fue una forma de pasar un solo parámetro que la función puede indicar que tiene que implementar todas las interfaces en ese tipo. Me pregunto si puedo hacerlo con una "restricción genérica". Déjenme intentarlo.
Daisha Lynn
1
Funcionó. Aquí está funcionando: dotnetfiddle.net/cfmDbs .
Daisha Lynn
1

Creo que harías bien en aprender sobre Redux o Elm y cómo manejan esta pregunta.

Básicamente, tiene una función pura que toma todo el estado y la acción que realizó el usuario y devuelve el nuevo estado.

Esa función luego llama a otras funciones puras, cada una de las cuales maneja una parte particular del estado. Dependiendo de la acción, muchas de estas funciones pueden hacer nada más que devolver el estado original sin cambios.

Para obtener más información, busque en Google the Elm Architecture o Redux.js.org.

Daniel T.
fuente
No sé Elm, pero creo que es similar a Redux. En Redux, ¿no se llama a todos los reductores por cada cambio de estado? Suena extremadamente ineficiente.
Daisha Lynn
Cuando se trata de optimizaciones de bajo nivel, no asuma, mida. En la práctica, es lo suficientemente rápido.
Daniel T.
Gracias Daniel, pero no funcionará para mí. He desarrollado lo suficiente como para saber que no notifica a todos los componentes de la interfaz de usuario cada vez que cambian los datos, independientemente de si el control se preocupa por el control.
Daisha Lynn
-2

Creo que lo que está tratando de hacer es usar un lenguaje orientado a objetos de una manera que no debería usarse, como si fuera puramente funcional. No es que los idiomas OO fueran todos malvados. Existen ventajas de cualquiera de los enfoques, por eso ahora podemos mezclar el estilo OO con el estilo funcional y tener la oportunidad de hacer que algunas piezas de código sean funcionales, mientras que otras permanecen orientadas a objetos para que podamos aprovechar toda la reutilización, herencia o polimofismo. Afortunadamente, ya no estamos obligados a ninguno de los dos enfoques, ¿por qué estás tratando de limitarte a uno de ellos?

Respondiendo a su pregunta: no, no entretejo ningún estado en particular a través de la lógica de la aplicación, pero uso lo que es apropiado para el caso de uso actual y aplico las técnicas disponibles de la manera más apropiada.

C # no está listo (todavía) para usarse tan funcional como le gustaría que fuera.

t3chb0t
fuente
3
No emocionado con esta respuesta, ni el tono. No estoy abusando de nada. Estoy empujando los límites de C # para usarlo como un lenguaje más funcional. No es algo raro de hacer. Parece que te opones filosóficamente, lo cual está bien, pero en ese caso, no mires esta pregunta. Tu comentario no sirve para nadie. Siga adelante.
Daisha Lynn
@DaishaLynn te equivocas, no me opongo de ninguna manera y, de hecho, lo uso mucho ... pero donde es natural y posible y no trato de convertir un lenguaje OO en uno funcional solo porque es moderno para hacerlo No tiene que estar de acuerdo con mi respuesta, pero eso no cambia el hecho de que no está utilizando correctamente sus herramientas.
t3chb0t
No lo estoy haciendo porque está de moda hacerlo. C # se está moviendo hacia el uso de un estilo funcional. Anders Hejlsberg mismo lo ha indicado como tal. Entiendo que solo está interesado en el uso generalizado del lenguaje, y entiendo por qué y cuándo es apropiado. Simplemente no sé por qué alguien como tú está incluso en este hilo ... ¿Cómo estás ayudando realmente?
Daisha Lynn
@DaishaLynn si no puede hacer frente a las respuestas que critican su pregunta o enfoque, probablemente no debería hacer preguntas aquí o la próxima vez debería simplemente agregar un descargo de responsabilidad que diga que solo está interesado en respuestas que respalden su idea al 100% porque no querer escuchar la verdad, sino obtener opiniones de apoyo.
t3chb0t
Por favor, sean un poco más cordiales el uno con el otro. Es posible hacer críticas sin menospreciar el lenguaje. Intentar programar C # en un estilo funcional ciertamente no es "abuso" o un caso marginal. Es una técnica común utilizada por muchos desarrolladores de C # para aprender de otros lenguajes.
zumalifeguard