¿Cómo construyo un sistema que tenga todo lo siguiente ?
- Usar funciones puras con objetos inmutables.
- 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)
- Evite tener demasiados argumentos para las funciones.
- 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.
fuente
Respuestas:
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:
GameState
jerarquía, para obtener 3-5 partes más pequeñas en lugar de 15.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?
fuente
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
podría definir interfaces
IHasMyInt
yIHasMyString
con métodosGetMyInt
yGetMyString
respectivamente. La clase de estado se ve así: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.
fuente
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.
fuente
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.
fuente