unitario tipos son simple qué que pais las formas federal estado ejemplos cuales compuesto comprende complejo haskell functional-programming state

tipos - Mantenimiento de estado complejo en Haskell



qué comprende el estado simple o unitario (2)

Supongamos que está construyendo una simulación bastante grande en Haskell. Hay muchos tipos diferentes de entidades cuyos atributos se actualizan a medida que avanza la simulación. Digamos, por el bien del ejemplo, que sus entidades se llaman Monos, Elefantes, Osos, etc.

¿Cuál es su método preferido para mantener los estados de estas entidades?

El primer y más obvio enfoque en el que pensé fue esto:

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''

Ya es feo tener cada tipo de entidad explícitamente mencionado en la firma de la función mainLoop . Puedes imaginar cómo sería absolutamente horrible si tuvieras, digamos, 20 tipos de entidades. (20 no es irrazonable para simulaciones complejas.) Entonces, creo que este es un enfoque inaceptable. Pero su gracia salvadora es que las funciones como updateMonkeys son muy explícitas en lo que hacen: toman una lista de Monos y devuelven uno nuevo.

Entonces, el siguiente pensamiento sería convertir todo en una gran estructura de datos que contenga todo el estado, limpiando así la firma de mainLoop :

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

Algunos sugieren que envolvemos GameState en una updateMonkeys estado y llamamos a updateMonkeys etc. en un do . Esta bien. Algunos preferirían sugerir que lo limpiemos con la composición de la función. También está bien, creo. (Por cierto, soy un novato con Haskell, así que tal vez estoy equivocado sobre algo de esto).

Pero luego el problema es que funciones como updateMonkeys no le dan información útil de su firma de tipo. Realmente no puedes estar seguro de lo que hacen. Claro, updateMonkeys es un nombre descriptivo, pero eso es un pequeño consuelo. Cuando paso por un objeto divino y digo "por favor actualicen mi estado global", siento que volvemos al mundo imperativo. Se siente como variables globales con otro nombre: tiene una función que hace algo al estado global, lo llama, y ​​espera lo mejor. (Supongo que aún evitará algunos problemas de concurrencia que estarían presentes con las variables globales en un programa imperativo. Pero meh, la concurrencia no es casi lo único malo con las variables globales).

Otro problema es este: supongamos que los objetos necesitan interactuar. Por ejemplo, tenemos una función como esta:

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

Digamos que esto se llama en updateElephants , porque es allí donde verificamos si alguno de los elefantes está en el rango de pisoteo de cualquier mono. ¿Cómo se propagan elegantemente los cambios tanto a los monos como a los elefantes en este escenario? En nuestro segundo ejemplo, updateElephants toma y devuelve un objeto god, por lo que podría afectar a ambos cambios. Pero esto solo enturbia las aguas y refuerza mi punto de vista: con el objeto de Dios, en realidad estás simplemente mutando las variables globales. Y si no estás utilizando el objeto de dios, no estoy seguro de cómo propagarías esos tipos de cambios.

¿Qué hacer? Seguramente muchos programas necesitan administrar un estado complejo, así que supongo que hay algunos enfoques bien conocidos para este problema.

Solo por el bien de la comparación, así es como podría resolver el problema en el mundo OOP. Habría objetos de Monkey , Elephant , etc. Probablemente tenga métodos de clase para hacer búsquedas en el conjunto de todos los animales vivos. Tal vez podrías buscar por ubicación, por ID, lo que sea. Gracias a las estructuras de datos subyacentes a las funciones de búsqueda, permanecerían asignados en el montón. (Estoy asumiendo el recuento GC o de referencia). Sus variables miembro se mutarían todo el tiempo. Cualquier método de cualquier clase podría mutar cualquier animal vivo de cualquier otra clase. Por ejemplo, un Elephant podría tener un método de stomp que disminuiría la salud de un objeto Monkey pasado, y no habría necesidad de pasarlo

Del mismo modo, en un diseño Erlang u otro orientado al actor, puede resolver estos problemas con bastante elegancia: cada actor mantiene su propio bucle y, por lo tanto, su propio estado, por lo que nunca necesitará un objeto divino. Y el paso de mensajes permite que las actividades de un objeto desencadenen cambios en otros objetos sin pasar un montón de cosas en toda la pila de llamadas. Sin embargo, he oído decir que los actores en Haskell están desaprobados.


La respuesta es la programación reactiva funcional (FRP). Es un híbrido de dos estilos de codificación: gestión del estado de los componentes y valores dependientes del tiempo. Como FRP es en realidad toda una familia de patrones de diseño, quiero ser más específico: recomiendo Netwire .

La idea subyacente es muy simple: escribe muchos componentes pequeños y autónomos, cada uno con su propio estado local. Esto es prácticamente equivalente a los valores dependientes del tiempo, ya que cada vez que consulta dicho componente puede obtener una respuesta diferente y provocar una actualización de estado local. Luego combina esos componentes para formar su programa real.

Si bien esto suena complicado e ineficiente, en realidad es solo una capa muy delgada alrededor de las funciones normales. El patrón de diseño implementado por Netwire está inspirado en AFRP (Arrowized Functional Reactive Programming). Probablemente sea lo suficientemente diferente como para merecer su propio nombre (¿WFRP?). Es posible que desee leer el tutorial .

En cualquier caso, sigue una pequeña demostración. Tus bloques de construcción son cables:

myWire :: WireP A B

Piense en esto como un componente. Es un valor variable en el tiempo de tipo B que depende de un valor variable en el tiempo de tipo A , por ejemplo, una partícula en un simulador:

particle :: WireP [Particle] Particle

Depende de una lista de partículas (por ejemplo, todas las partículas existentes actualmente) y es en sí misma una partícula. Usemos un cable predefinido (con un tipo simplificado):

time :: WireP a Time

Este es un valor variable en el tiempo de tipo Tiempo (= Doble ). Bueno, es hora (comenzando en 0 contados desde que se inició la red de cable). Como no depende de otro valor variable en el tiempo, puedes alimentarlo como quieras, de ahí el tipo de entrada polimórfica. También hay alambres constantes (valores variables en el tiempo que no cambian con el tiempo):

pure 15 :: Wire a Integer -- or even: 15 :: Wire a Integer

Para conectar dos cables, simplemente use la composición categórica:

integral_ 3 . 15

Esto le da un reloj a 15 veces la velocidad en tiempo real (la integral de 15 en el tiempo) comenzando en 3 (la constante de integración). Gracias a varias instancias de clase, los cables son muy útiles para combinar. Puede utilizar sus operadores habituales, así como estilo aplicativo o estilo de flecha. ¿Quieres un reloj que comience en 10 y sea el doble de la velocidad en tiempo real?

10 + 2*time

¿Quieres una partícula que comience y (0, 0) con (0, 0) velocidad y acelere con (2, 1) por segundo por segundo?

integral_ (0, 0) . integral_ (0, 0) . pure (2, 1)

¿Desea mostrar estadísticas mientras el usuario presiona la barra espaciadora?

stats . keyDown Spacebar <|> "stats currently disabled"

Esto es solo una pequeña fracción de lo que Netwire puede hacer por usted.


Sé que este es un tema viejo. Pero estoy enfrentando el mismo problema ahora mismo al intentar implementar el ejercicio de cifrado Rail Fence de exercism.io. Es bastante decepcionante ver un problema tan común que tiene tan poca atención en Haskell. No considero que hacer algo tan simple como mantener el estado. Necesito aprender FRP. Entonces, continué buscando en Google y encontré la solución más sencilla: mónada estatal: https://en.wikibooks.org/wiki/Haskell/Understanding_monads/State