2011-11-03 6 views
17

Stavo giocando con la monade di stato e non so cosa stia causando l'overflow dello stack in questo semplice pezzo di codice.Perché questo semplice uso della monade di stato causa uno stack overflow?

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 Vorrei solo sapere che cosa sta causando il problema in questo pezzo di codice, il compito in sé non è importante di per sé.

+6

Non correlato alla tua domanda attuale: potresti "replicareM". –

+3

Vedo 'import Control.Monad.State.Lazy' e' put $! (n + 1) 'e sono immediatamente sospettoso ... –

+1

@DanBurton In realtà era' Control.Monad.State' all'inizio e poi l'ho trovato uguale a 'C.M.S.Lazy', quindi l'ho modificato. Ho dimenticato tutto su 'C.M.S.Strict' però :) – haskelline

risposta

41

Il problema è che Control.Monad.State.Lazy's (>> =) è così pigro che nemmeno il ($!) Non ti aiuta.
Prova Control.Monad.State.Strict, che dovrebbe raggiungere ($!).

Il (>> =) della monade di stato lazy non guarda affatto alla coppia (valore, stato), quindi l'unico modo per ottenere una valutazione eseguita prima della fine è il f in m >>= f decostruisci la coppia. Questo non succede qui, quindi ottieni un enorme thunk, che è troppo grande per lo stack quando runState vuole finalmente un risultato.

Ok, ho mangiato, ora posso elaborare. Consentitemi di usare la vecchia definizione (mtl-1.x) della monade pigra State s, è un po 'più facile vederla senza la monade interiore. La nuova definizione (mtl-2.x) type State s = StateT s Identity si comporta allo stesso modo, è solo più di scrittura e lettura. La definizione di (>> =) era

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

Ora, let attacchi sono pigri, quindi questa è

m >>= k = State $ \s -> let 
    blob = runState m s 
    in runState (k $ fst blob) (snd blob) 
solo più leggibile

. Quindi (>> =) lascia il blob completamente non valutato. La valutazione è necessaria solo se k ha bisogno di ispezionare fst blob per determinare come continuare, oppure k a ha bisogno di ispezionare snd blob.

In replicateM r tick, i calcoli sono concatenati con (>>), quindi la definizione k in (>> =) è const tick. Come funzione costante, non ha assolutamente bisogno di esaminare la sua argomentazione. Così tick >> tick diventa

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 

il seq non viene toccato fino blobN deve essere valutata. Ma la necessità di valutarlo per il costruttore più esterno - il costruttore di coppie (,) - sarebbe sufficiente per attivare il seq e questo porterebbe a sua volta alla valutazione completa qui. Ora, in million, nulla richiede alcuna valutazione fino al snd finale dopo il raggiungimento dello runState. A quel punto, è stato costruito un thunk con un milione di strati. Valutare che thunk richiede spingere molti let m' = m+1 in seq m' ((),m') nello stack fino a quando non viene raggiunto lo stato iniziale e se lo stack è abbastanza grande da contenere, verranno quindi inseriti e applicati. Quindi sarebbero tre traversamenti, 1. costruire il thunk, 2. staccare gli strati dal thunk e spingerli sullo stack, 3. consumare lo stack.

Il (>> =) di Control.Monad.State.Strict è abbastanza rigoroso da forzare il seq s su ciascun bind, quindi c'è solo un attraversamento, nessun (non banale) thunk è costruito e il calcolo viene eseguito in modo costante spazio. La definizione è

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

La differenza importante è che il pattern matching in case espressioni è rigorosa, qui il blob deve essere valutata al costruttore esterno per abbinare contro il modello nel case.
Con m = tick = State (\m -> let m' = m+1 in seq m' ((),m')) parte essenziale diventa

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

Il pattern match richiede la valutazione di ((), s') [al (,) costruttore], dal seq che è legato alla valutazione di s' = s+1, tutto è completamente valutata ogni legame, niente thunk, niente stack.

Tuttavia, è comunque necessario fare attenzione. In questo caso, a causa dello seq (rispettivamente ($!)) e della struttura superficiale dei tipi coinvolti, la valutazione è stata mantenuta con l'applicazione di (>>). In generale, con tipi strutturati più profondi e/o senza seq s, C.M.S.Strict crea anche thunk di grandi dimensioni che possono portare a overflow dello stack. I thunk sono un po 'più semplici e meno aggrovigliati di quelli generati da C.M.S.Lazy in quelle circostanze.

D'altra parte, la pigrizia di C.M.S.Lazy consente altri calcoli impossibili con C.M.S.Strict. Ad esempio, C.M.S.Lazy fornisce una delle pochissime monadi in cui

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

termina. [Ma sii consapevole che lo stato è quindi inutilizzabile; prima che potesse essere usato, avrebbe dovuto percorrere l'intera lista infinita. Quindi, se fai qualcosa del genere, prima di poter riprendere i calcoli dipendenti dallo stato, devi put uno stato nuovo.]

+2

Grazie mille per questa spiegazione dettagliata. Ho notato anche che nelle fonti 'C.M.S.Lazy' utilizza i pattern lazy mentre' C.M.S.Strict' no e questo è ciò che sta causando la differenza nella versione corrente. La tua spiegazione con la vecchia versione è più chiara, grazie ancora. – haskelline

+0

Nella tua risposta [qui] (http://stackoverflow.com/a/8763702/722938), dovevi usare esplicitamente la corrispondenza dei pattern pigri, ma nella tua spiegazione precedente hai menzionato che i binding sono pigri. Potresti per favore approfondire la differenza tra i due casi? – haskelline

+1

In questa risposta, il modello pigro è un argomento di funzione. L'argomento della funzione in una definizione di funzione, indipendentemente dal fatto che la funzione sia vincolata o meno in un let, causa una corrispondenza di modello quando viene chiamata la funzione. La corrispondenza del modello è rigorosa, a meno che il modello non sia irrefutabile (una variabile, un carattere jolly o un pattern '~ '). Dato che lì la funzione diventa l'argomento di 'fix', non deve essere rigoroso, quindi il suo argomento deve essere un modello inconfutabile. Invece di '~ (st: sts)', si potrebbe usare una variabile e decostruirla con 'head' e' tail', ma '~ (st: sts)' è più bello. –