2012-03-03 10 views
21

Tengo una pregunta sobre la mejor manera de diseñar un programa en el que estoy trabajando en Haskell. Estoy escribiendo un simulador de la física, que es algo que he hecho un montón de lenguajes imperativos estándar, y por lo general el método principal se ve algo como:Diseño de programa en Haskell: cómo hacer simulación sin mutabilidad

while True: 
    simulationState = stepForward(simulationState) 
    render(simulationState) 

Y me pregunto cómo hacer algo similar en Haskell . Tengo una función step :: SimState -> SimState y una función display :: SimState -> IO() que utiliza HOpenGL para dibujar un estado de simulación, pero no sé cómo hacer esto en un "bucle" de clases, ya que todas las soluciones que puedo encontrar implican algún tipo de mutabilidad Soy un poco novato en lo que respecta a Haskell, por lo que es muy posible que me pierda una decisión de diseño muy obvia. Además, si hay una mejor manera de diseñar mi programa como un todo, me gustaría escucharlo.

¡Gracias de antemano!

Respuesta

20

En mi opinión, la forma correcta de pensar en este problema no es como un bucle, sino como una lista u otra estructura de transmisión infinita. Di a similar answer a a similar question; la idea básica es, como C. A. McCann wrote, 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 hacer frente a un paso monadic, y en particular a una función de renderización monádica. Un enfoque sería simplemente tomar el fragmento deseado de la lista por adelantado (posiblemente con una función como takeWhile, posiblemente con recursión manual) y luego mapM_ render en eso. Un mejor enfoque sería usar una estructura de transmisión diferente, intrínsecamente monádica. Los cuatro que se me ocurren son:

  • The iteratee package, que se diseñó originalmente para la transmisión de E/S. Creo que aquí, sus pasos serían una fuente (enumerator) y su representación sería un sumidero (iteratee); luego puede usar un tubo (un enumeratee) para aplicar funciones y/o filtrar en el medio.
  • The enumerator package, basado en las mismas ideas; uno podría ser más limpio que el otro.
  • The newer pipes package, que se anuncia como “iteratees hecho justo” -Es más nuevo, pero la semántica es, al menos para mí, mucho más clara, al igual que los nombres (Producer, Consumer y Pipe).
  • The List package, en particular su ListT transformador de mónada. Este transformador de mónada está diseñado para permitirle crear listas de valores monádicos con una estructura más útil que [m a]; por ejemplo, trabajar con listas monádicas infinitas se vuelve más manejable. El paquete también generaliza muchas funciones en listas en a new type class. Proporciona una función iterateM dos veces; el first time en generalidad increíble, y el second time especializado en ListT. A continuación, puede usar funciones como takeWhileM para filtrar.

La gran ventaja de reificante iteración de su programa de alguna estructura de datos, en lugar de simplemente utilizando la recursividad, 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 "cómo generar". Ahora, el usuario (incluso si es solo usted) puede decidir por separado cuándo detenerse: después de n pasos? ¿Después de que el estado satisface un cierto predicado? No hay razón para empantanar su código de generación con estas decisiones, ya que lógicamente es una preocupación separada.

+1

Parece que falta su lista [el paquete 'monad-loops'] (http://hackage.haskell.org/package/monad-loops), que creo que es en realidad la demostración más clara del enfoque. –

+0

Fantástico - He estado buscando una razón para aprender iteratees. Echaré un vistazo al paquete de tuberías. ¡Muchas gracias! –

+3

es excesivo para la pregunta original, pero para aquellos que podrían venir después creo que deberíamos mencionar [Programación reactiva funcional] (http://stackoverflow.com/questions/3154701/help-understanding-arrows-in- haskell) en particular [Yampa/Ánimas] (http://www.haskell.org/haskellwiki/Yampa). –

11

Su enfoque está bien, sólo tiene que recordar que los bucles se expresan como la recursividad en Haskell:

simulation state = do 
    let newState = stepForward state 
    render newState 
    simulation newState 

(. Pero hay que definietly un criterio de cómo terminar el bucle)

+0

Solo para confirmar, ¿esto no acumulará desbordamiento porque es recursividad de cola? –

+3

No es recursivo de cola ni debe acumular desbordamiento :) Pruébelo o pruebe con una de las otras soluciones que secuencian una lista de estados procesados. – Ingo

+7

@haldean No se desbordará la pila, aunque por diferentes motivos. La recursividad de la cola no es tan útil o importante en Haskell como en otros idiomas, debido a la pereza. – delnan

20

Bueno, si dibuja estados sucesivos es todos que desea hacer, eso es bastante simple. Primero, tome su función step y el estado inicial y use the iterate function. iterate step initialState es entonces una lista (infinita) de cada estado de simulación. A continuación, puede asignar display sobre eso para conseguir acciones IO para dibujar cada estado, por lo que en conjunto tendría algo como esto:

allStates :: [SimState] 
allStates = iterate step initialState 

displayedStates :: [IO()] 
displayedStates = fmap display allStates 

La forma más sencilla de ejecutar sería a continuación, utilizar the intersperse function poner un "retraso "la acción entre cada acción de la pantalla, a continuación, utilizar the sequence_ function para funcionar todo el asunto:

main :: IO() 
main = sequence_ $ intersperse (delay 20) displayedStates 

Por supuesto que significa que tiene que terminar por la fuerza de la aplicación y se opone a cualquier tipo de interactividad, así que no es realmente una buena manera de hacerlo en general.

Un enfoque más sensato sería intercalar cosas como "ver si la aplicación debe salir" en cada paso. Usted puede hacer eso 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 los pasos no recursivos lugar y luego usar un combinador de bucle más abstracto. Por desgracia, no hay realmente un buen apoyo para hacerlo de esa manera en las bibliotecas estándar, pero sería algo como esto:

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 ejercicio para el lector, je.

+0

La solución iterate/fmap es increíble, pero voy a ir con el método de recisión. ¡Muchas gracias! –

Cuestiones relacionadas