2010-07-30 6 views
10

J'ai enfin réussi à utiliser les monades (je ne sais pas si je les comprends ...), mais mon code n'est jamais très élégant. Je suppose est d'un manque d'emprise sur la façon dont toutes ces fonctions sur Control.Monad peuvent vraiment aider. Donc, je pensais que ce serait bien de demander des conseils à ce sujet dans un morceau de code particulier en utilisant la monade d'état.Conseils pour un code plus élégant avec des monades?

Le but du code est de calculer de nombreux types de marches aléatoires, et c'est quelque chose que j'essaie de faire avant quelque chose de plus compliqué. Le problème est que j'ai deux calculs stateful en même temps, et je voudrais savoir comment les composer avec élégance:

  1. La fonction qui met à jour le générateur de nombres aléatoires est quelque chose de type Seed -> (DeltaPosition, Seed)
  2. La fonction qui met à jour la position du marcheur aléatoire est quelque chose de type DeltaPosition -> Position -> (Log, Position) (où Log est juste un moyen pour moi de signaler quelle est la position actuelle du marcheur aléatoire).

Ce que je l'ai fait est la suivante:

J'ai une fonction pour composer ces deux calculs: 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)) 

puis je en faire une fonction qui composent les états:

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 

Et puis j'ai la chose la plus simple, par exemple, un marcheur aléatoire qui va simplement additionner un nombre aléatoire à sa position actuelle:

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 

et une fonction pour ce faire à plusieurs reprises:

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

Et puis, si je charge cela dans ghci et lancez:

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

Je reçois ce que je veux, ce qui est liste des positions occupées par le marcheur aléatoire. Mais ... je pense qu'il doit y avoir une façon plus élégante de faire cela. J'ai deux questions:

  1. Puis-je réécrire ces fonctions d'une manière plus « monade », en utilisant des fonctions intelligentes de Control.Monad?

  2. Existe-t-il un modèle général de combinaison d'états comme celui-ci pouvant être utilisé? Est-ce que cela a quelque chose à voir avec les transformateurs monad ou quelque chose comme ça?

+2

Par ailleurs, il est une bonne idée d'éviter d'utiliser le 'constructeur de données State', puisque dans' successeur de mtl' ('monades-fd'),' State' est défini en termes de 'StateT' et donc le constructeur de données' State' n'existe pas. –

+0

@TravisBrown En fait, 'monads-fd' est déprécié en faveur de' mtl'. (Reconnaissant que votre commentaire a 5 ans.) – crockeea

Répondre

11

Mise à jour: je l'ai mentionné qu'il ya en fait beaucoup plus agréable façon de faire qui ne nécessite pas State ou monades à tous:

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) 

Il fonctionne comme vous le souhaitez:

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

Si vous n'êtes pas engagé à l'idée de "composer" deux calculs d'état séparés, vous pouvez accomplir la même chose beaucoup plus simplement:

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) 

Cela produit le même résultat que votre exemple:

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

Cette approche (faire toutes les manipulations de l'Etat dans un seul monade au lieu d'essayer de composer un State A et State B) me semble être la solution la plus élégante.


Mise à jour: Pour répondre à votre question sur l'utilisation des transformateurs de monades pour empiler State monades: il est certainement possible. Nous pouvons écrire ce qui suit, par exemple:

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 

Nous pourrions également faire l'empilement dans l'ordre inverse.

Cette version produit à nouveau la même sortie, mais à mon avis la version non StateT est un peu plus claire.

1

La façon habituelle de composer 2 monades (et la seule façon pour la plupart des monades) est avec les transformateurs monad, mais avec différentes monades State vous avez plus d'options. Par exemple: vous pouvez utiliser ces fonctions:

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')) 
Questions connexes