2010-07-30 15 views
10

Finalmente ho imparato come usare le monadi (non so se le capisco ...), ma il mio codice non è mai molto elegante. Immagino sia da una mancanza di presa su come tutte quelle funzioni su Control.Monad possono davvero aiutare. Quindi ho pensato che sarebbe stato carino chiedere consigli su questo in un particolare pezzo di codice usando la monade di stato.Suggerimenti per un codice più elegante con le monadi?

L'obiettivo del codice è calcolare molti tipi di percorsi casuali, ed è qualcosa che sto cercando di fare prima di qualcosa di più complicato. Il problema è che ho due calcoli stateful allo stesso tempo, e mi piacerebbe sapere come comporre loro con eleganza:

  1. La funzione che aggiorna il generatore di numeri casuali è qualcosa di tipo Seed -> (DeltaPosition, Seed)
  2. La funzione che aggiorna la posizione del walker casuale è qualcosa di tipo DeltaPosition -> Position -> (Log, Position) (dove Log è solo un modo per me di segnalare qual è la posizione corrente del walker casuale).

Quello che ho fatto è questo:

Ho una funzione di comporre questi due calcoli stateful:

composing :: (g -> (b, g)) -> (b -> s -> (v,s)) -> (s,g) -> (v, (s, g)) 
composing generate update (st1, gen1) = let (rnd, gen2) = generate gen1 
              (val, st2) = update rnd st1 
             in (val, (st2, gen2)) 

e poi mi trasformarlo in una funzione che compongono gli stati:

stateComposed :: State g b -> (b -> State s v) -> State (s,g) v 
stateComposed rndmizer updater = let generate = runState rndmizer 
            update x = runState $ updater x 
           in State $ composing generate update 

E poi ho la cosa più semplice, ad esempio, un walker casuale che somma solo un numero casuale alla sua posizione corrente:

update :: Double -> State Double Double 
update x = State (\y -> let z = x+y 
         in (z,z)) 

generate :: State StdGen Double 
generate = State random 

rolling1 = stateComposed generate update 

e una funzione per fare questo più volte:

rollingN 1 = liftM (:[]) rolling1 
rollingN n = liftM2 (:) rolling1 rollings 
    where rollings = rollingN (n-1) 

E poi, se io carico questo ghci e corro:

*Main> evalState (rollingN 5) (0,mkStdGen 0) 
[0.9872770354820595,0.9882724161698186,1.9620425108498993,2.0923229488759123,2.296045158010918] 

ottengo quello che voglio, che è un elenco delle posizioni occupate dal walker casuale. Ma ... sento che ci deve essere un modo più elegante per farlo. Ho due domande:

  1. possibile riscrivere queste funzioni in un modo più "monade", utilizzando le funzioni intelligenti da Control.Monad?

  2. Esiste un motivo generale per combinare stati come questo che possono essere utilizzati? Questo ha qualcosa a che fare con i trasformatori monad o qualcosa del genere?

+2

Tra l'altro, è una buona idea per evitare di usare il costruttore di dati 'State', dal momento che in' successore di mtl' ('monadi-fd'),' State' è definito in termini di 'StateT' e quindi il costruttore di dati' State' non esiste. –

+0

@TravisBrown In realtà, 'monads-fd' è deprecato a favore di' mtl'. (Riconoscendo che il tuo commento ha 5 anni.) – crockeea

risposta

11

Update: Avrei detto che non c'è in realtà un modo molto più piacevole per fare questo che non richiede State o monadi a tutti:

takeStep :: (Double, StdGen) -> (Double, StdGen) 
takeStep (p, g) = let (d, g') = random g in (p + d, g') 

takeSteps n = take n . tail . map fst $ iterate takeStep (0, mkStdGen 0) 

funziona come desiderato:

*Main> takeSteps 5 
[0.9872770354820595,0.9882724161698186,1.9620425108498993,2.0923229488759123,2.296045158010918] 

Se non si è impegnati a "comporre" due calcoli stateful separati, è possibile eseguire la stessa cosa molto più semplicemente:

takeStep :: State (Double, StdGen) Double 
takeStep = do 
    (pos, gen) <- get 
    let (delta, gen') = random gen 
    let pos' = pos + delta 
    put (pos', gen') 
    return pos' 

takeSteps n = evalState (replicateM n takeStep) (0, mkStdGen 0) 

Ciò produce lo stesso output come il tuo esempio:

*Main> takeSteps 5 
[0.9872770354820595,0.9882724161698186,1.9620425108498993,2.0923229488759123,2.296045158010918] 

Questo approccio (facendo tutta la manipolazione dello stato in una singola monade invece di cercare di comporre un State A e State B) mi sembra la soluzione più elegante.


Update: Per rispondere alla tua domanda sull'uso di trasformatori monade di impilare State monadi: è certamente possibile. Possiamo scrivere quanto segue, ad esempio:

update' :: (Monad m) => Double -> StateT Double m Double 
update' x = StateT $ \y -> let z = x + y in return (z, z) 

generate' :: (Monad m) => StateT StdGen m Double 
generate' = StateT $ return . random 

takeStep' :: StateT Double (State StdGen) Double 
takeStep' = update' =<< lift generate' 

takeSteps' n = evalState (evalStateT (replicateM n takeStep') 0) $ mkStdGen 0 

Possiamo anche eseguire l'impilaggio nell'ordine inverso.

Questa versione produce nuovamente lo stesso risultato, ma a mio parere la versione non StateT è un po 'più chiara.

1

Il solito modo di comporre 2 monadi (e l'unico modo per la maggior parte delle monadi) è con i trasformatori monad, ma con diverse monadi State hai più opzioni. Ad esempio: è possibile utilizzare queste funzioni:

leftState :: State a r -> State (a,b) r 
leftState act = state $ \ ~(a,b) -> let 
    (r,a') = runState act a 
    in (r,(a',b)) 

rightState :: State b r -> State (a,b) r 
rightState act = state $ \ ~(a,b) -> let 
    (r,b') = runState act b 
    in (r,(a,b'))