pattern - Diseño del programa en Haskell: cómo hacer simulación sin mutabilidad
pattern matching haskell (3)
Bueno, si lo único que quieres hacer es dibujar estados sucesivos, es bastante simple. Primero, tome su función de step
y el estado inicial y use la función iterate
. iterate step initialState
es entonces una lista (infinita) de cada estado de simulación. Luego, puede asignar la display
sobre eso para obtener acciones de E / S para dibujar cada estado, para que juntos tengan algo como esto:
allStates :: [SimState]
allStates = iterate step initialState
displayedStates :: [IO ()]
displayedStates = fmap display allStates
La forma más sencilla de ejecutarlo sería utilizar la función de intersperse
para poner una acción de "demora" entre cada acción de visualización, luego usar la función de sequence_
para ejecutar todo el proceso:
main :: IO ()
main = sequence_ $ intersperse (delay 20) displayedStates
Por supuesto, eso significa que debe finalizar la aplicación a la fuerza y excluir cualquier tipo de interactividad, por lo que no es una buena forma de hacerlo en general.
Un enfoque más sensato sería intercalar cosas como "ver si la aplicación debería salir" en cada paso. Puedes hacerlo con recursión explícita:
runLoop :: SimState -> IO ()
runLoop st = do display st
isDone <- checkInput
if isDone then return ()
else delay 20 >> runLoop (step st)
Mi enfoque preferido es escribir pasos no recursivos en su lugar y luego usar un combinador de bucle más abstracto. Desafortunadamente, no hay realmente un buen soporte para hacerlo de esa manera en las bibliotecas estándar, pero se vería algo así:
runStep :: SimState -> IO SimState
runStep st = do display st
delay 20
return (step st)
runLoop :: SimState -> IO ()
runLoop initialState = iterUntilM_ checkInput runStep initialState
La implementación de la función iterUntilM_
se deja como un ejercicio para el lector, je.
Tengo una pregunta sobre la mejor manera de diseñar un programa en el que estoy trabajando en Haskell. Estoy escribiendo un simulador de física, que es algo que he hecho un montón en lenguajes imperativos estándar, y por lo general el método principal es algo así como:
while True:
simulationState = stepForward(simulationState)
render(simulationState)
Y me pregunto cómo hacer algo similar en Haskell. Tengo un step :: SimState -> SimState
función step :: SimState -> SimState
y una display :: SimState -> IO ()
función display :: SimState -> IO ()
que usa HOpenGL para dibujar un estado de simulación, pero no sé cómo hacer esto en un "bucle" En cierto modo, ya que todas las soluciones que se me ocurren implican algún tipo de mutabilidad. Soy un poco novato cuando se trata de Haskell, por lo que es muy posible que me esté perdiendo una decisión de diseño muy obvia. Además, si hay una mejor manera de diseñar mi programa en su conjunto, me encantaría escucharlo.
¡Gracias por adelantado!
En mi opinión, la forma correcta de pensar sobre este problema no es como un bucle, sino como una lista u otra estructura de transmisión infinita. Di una respuesta similar a una pregunta similar ; La idea básica es, como escribió CA McCann , usar iterate stepForward initialState
, donde iterate :: (a -> a) -> a -> [a]
“devuelve una lista infinita de aplicaciones repetidas de [ stepForward
] a [ initialState
] ".
El problema con este enfoque es que tiene problemas para manejar un paso monádico , y en particular una función de representación monádica. Un enfoque sería tomar el fragmento deseado de la lista de antemano (posiblemente con una función como takeWhile
, posiblemente con recursión manual) y luego mapM_ render
en eso. Un mejor enfoque sería utilizar una estructura de transmisión diferente, intrínsecamente monádica. Los cuatro que puedo pensar son:
- El paquete iteratee , que fue diseñado originalmente para la transmisión de IO. Creo que aquí, sus pasos serían una fuente (
enumerator
) y su representación sería un sumidero (iteratee
); luego podría usar una tubería (unenumeratee
) para aplicar funciones y / o hacer filtrado en el medio. - El paquete del enumerador , basado en las mismas ideas; Uno podría estar más limpio que el otro.
- El paquete de tuberías más nuevo , que se factura a sí mismo como "iterado bien", es más nuevo, pero la semántica es, al menos para mí, significativamente más clara, al igual que los nombres (
Producer
,Consumer
yPipe
). - El paquete List , en particular su transformador de mónada
ListT
. Este transformador de mónada está diseñado para permitirle crear listas de valores monádicos con una estructura más útil que[ma]
; por ejemplo, trabajar con listas monádicas infinitas se vuelve más manejable. El paquete también generaliza muchas funciones en listas en una nueva clase de tipos . Proporciona una funcióniterateM
dos veces; La primera vez en increíble generalidad, y la segunda vez especializada enListT
. Luego puede usar funciones comotakeWhileM
para hacer su filtrado.
La gran ventaja de reificar la iteración de su programa en alguna estructura de datos, en lugar de simplemente usar la recursión, es que su programa puede hacer cosas útiles con el flujo de control. Nada demasiado grandioso, por supuesto, pero por ejemplo, separa la decisión de "cómo terminar" del proceso de "cómo generar". Ahora, el usuario (incluso si solo es usted) puede decidir por separado cuándo detenerse: ¿después de n pasos? ¿Después de que el estado satisface un determinado predicado? No hay razón para atascar su código de generación con estas decisiones, ya que lógicamente es una preocupación independiente.
Su enfoque es correcto, solo debe recordar que los bucles se expresan como recursión en Haskell:
simulation state = do
let newState = stepForward state
render newState
simulation newState
(Pero definitivamente necesitas un criterio sobre cómo terminar el ciclo).