2011-11-03 11 views
17

Estaba jugando con la mónada de estado, y no sé qué está causando el desbordamiento de pila en esta simple pieza de código.¿Por qué este uso simple de la mónada de estado causa un desbordamiento de pila?

import Control.Monad.State.Lazy 

tick :: State Int Int 
tick = do n <- get 
     put $! (n+1) 
     return n 

million :: Int 
million = snd $ runState (mapM_ (const tick) [1..1000000]) 0 

main = print million 

Nota Me gustaría saber lo que está causando el problema de esta pieza de código, la tarea en sí no es importante per se.

+6

No relacionado con su pregunta real: es posible que le guste 'replicateM'. –

+3

Veo 'import Control.Monad.State.Lazy' y' put $! (n + 1) 'y sospecho de inmediato ... –

+1

@DanBurton En realidad era' Control.Monad.State' al principio y luego me pareció igual que 'C.M.S.Lazy', así que lo cambié. Sin embargo, me olvidé de 'C.M.S.Strict' :) – haskelline

Respuesta

41

El problema es que Control.Monad.State.Lazy's (>> =) es tan vago que incluso el ($!) No lo ayuda.
Pruebe Control.Monad.State.Strict, que debería alcanzar el ($!).

La (>> =) de la mónada de estados perezosos no se ve en absoluto en el par (valor, estado), por lo que la única forma de realizar una evaluación antes de alcanzar el final es teniendo f en m >>= f deconstruir el par Eso no sucede aquí, así que obtienes un gran thunk, que es demasiado grande para la pila cuando runState finalmente quiere un resultado.

Bien, he comido, ahora puedo dar más detalles. Permítanme usar la antigua definición (mtl-1.x) de la mónada State s perezosa, es un poco más fácil de ver allí sin la mónada interna. La nueva definición (mtl-2.x) type State s = StateT s Identity se comporta de la misma manera, es solo más escritura y lectura. La definición de (>> =) era

m >>= k = State $ \s -> let 
    (a, s') = runState m s 
    in runState (k a) s' 

Ahora, let fijaciones son perezosos, por lo tanto, esto es

m >>= k = State $ \s -> let 
    blob = runState m s 
    in runState (k $ fst blob) (snd blob) 
sólo es más legible

. Entonces, el (>> =) deja el blob completamente sin evaluar. La evaluación solo es necesaria si k necesita inspeccionar fst blob para determinar cómo continuar o k a necesita inspeccionar snd blob.

En replicateM r tick, los cálculos están encadenados con (>>), por lo que la definición k en (>> =) es const tick. Como función constante, definitivamente no necesita inspeccionar su argumento. Así se convierte en tick >> tick

State $ \s -> 
    let blob1 = (\n -> let n' = n+1 in seq n' ((),n')) s 
     blob2 = (\m -> let m' = m+1 in seq m' ((),m')) (snd blob1) 
    in blob2 

la seq no se toca hasta blobN tiene que ser evaluado. Pero la necesidad de evaluarlo al constructor más externo, el constructor de pares (,), sería suficiente para activar el seq y eso a su vez llevaría a una evaluación completa aquí. Ahora, en million, nada requiere ninguna evaluación hasta que se alcanza el snd final después del runState. En ese momento, se ha construido un procesador con un millón de capas. La evaluación de ese procesador requiere empujar muchos let m' = m+1 in seq m' ((),m') en la pila hasta que se alcance el estado inicial, y si la pila es lo suficientemente grande como para contenerlos, se abrirán y aplicarán. Así que serían tres recorridos, 1. construyendo el procesador, 2. quitando capas del procesador y empujándolos en la pila, 3. consumiendo la pila.

El (>> =) de Control.Monad.State.Strict es sólo lo suficientemente estrictas para obligar a los seq s en cada unen, por tanto, sólo hay un recorrido, no (no trivial) de procesador está construido y el cálculo se ejecuta en constante espacio. La definición es

m >>= k = State $ \s -> 
    case runState m s of 
     (a, s') -> runState (k a) s' 

La diferencia importante es que la coincidencia de patrones en case expresiones es estricta, aquí la blob tiene que ser evaluado para el constructor más externa para que coincida con el patrón en el case.
Con m = tick = State (\m -> let m' = m+1 in seq m' ((),m')) la parte esencial se convierte en

case let s' = s+1 in seq s' ((),s') of 
    (a, s'') -> runState (k a) s'' 

El patrón de adaptarse a las demandas de la evaluación de ((), s') [a la (,) constructor], por el seq que está ligado a la evaluación de s' = s+1, todo está completamente evaluado en cada enlace, no thunks, no stack.

Sin embargo, todavía debe tener cuidado. En este caso, debido a seq (resp. ($!)) y la estructura superficial de los tipos involucrados, la evaluación se mantuvo con la aplicación (>>). En general, con tipos estructurados más profundos y/o sin seq s, C.M.S.Strict también crea grandes thunk que pueden conducir al desbordamiento de la pila. Los thunk son simplemente un poco más simples y menos enredados que los generados por C.M.S.Lazy en esas circunstancias.

Por otro lado, la pereza de C.M.S.Lazy permite otros cálculos que son imposibles con C.M.S.Strict. Por ejemplo, C.M.S.Lazy proporciona una de las pocas mónadas donde

take 100 <$> mapM_ something [1 .. ] 

termina. [Pero tenga en cuenta que el estado es entonces inutilizable; antes de poder usarse, debería viajar por toda la lista infinita. Entonces, si hace algo como eso, antes de poder reanudar los cálculos dependientes del estado, tiene que put un nuevo estado.]

+2

Muchas gracias por esta explicación detallada. También noté que en las fuentes 'C.M.S.Lazy' usa patrones flojos mientras que' C.M.S.Strict' no y eso es lo que está causando la diferencia en la versión actual. Su explicación con la versión anterior es más clara, gracias de nuevo. – haskelline

+0

En su respuesta [aquí] (http://stackoverflow.com/a/8763702/722938), tenía que usar explícitamente la coincidencia de patrones perezosos, pero en la explicación anterior usted mencionó que los enlaces son flojos. ¿Podrían explicar la diferencia entre los dos casos? – haskelline

+1

En esa respuesta, el patrón diferido es un argumento de función. El argumento de función en una definición de función, independientemente de si la función está vinculada en let o no, provoca una coincidencia de patrón cuando se llama a la función. La coincidencia de patrones es estricta, a menos que el patrón sea irrefutable (una variable, un comodín o un patrón diferido '~ pattern'). Como allí la función se convierte entonces en el argumento de 'fix', no debe ser estricta, por lo que su argumento debe ser un patrón irrefutable. En lugar de '~ (st: sts)', se podría usar una variable y deconstruirla con 'head' y' tail', pero '~ (st: sts)' es más agradable. –

Cuestiones relacionadas