User:Davjam2:Example/StateMonad

From Wikibooks, open books for an open world
Jump to navigation Jump to search

Note

The text from this draft was copied into Haskell/Understanding_monads/State, and so I've deleted it from here.

This page now used for draft additions/amendments.


Handling Combined States[edit | edit source]

Suppose we wanted to create a random turnstile, where each visitor would be given a random turnstile input: either they insert a coin (but are not allowed through); or they get to push the arm (and go through if it opens, but are otherwise sent away).

Here's one useful bit of code:

randomInputS :: State StdGen TurnstileInput
randomInputS = do
  b <- getRandomS
  return $ if b then Coin else Push

This allows us to generate random turnstileInput values[1]. However, our random turnstile machine needs to track both the state of a random number generator and the state of the turnstile. We want to write a function like this:

randomTurnS :: State (StdGen, TurnstileState) TurnstileOutput

And this function needs to call both randomInputS (which is in the State StdGen monad) and turnS (which is in the State TurnstileState monad).

Exercises
  1. Implement randomTurnS, using get and put to access and set the combined state, and runState to invoke randomInputS and turnS.

Much of the code in randomTurnS deals with managing the state: accessing the combined state, unpacking subcomponents, forwarding them to the individual State monads, recombining them and putting the combined state back. The state management code is not too bad in this case, but could easily become cumbersome in a more complex function. And it is something we wanted the State monad to hide from us.

State-Processing a Subcomponent[edit | edit source]

Ideally we'd want some utility function(s) that allow us to invoke a State StdGen monad function (or State TurnstileState monad function) from within a State (StdGen, TurnstileState) monad function. These function(s) should take care of the state management for us, ensuring that the right subcomponent of the combined state is updated.

Here's one such a function that works for any combined state represented as a pair, and performs the state update on the fst of the pair:

processingFst :: State a o -> State (a,b) o
processingFst m = do
  (s1,s2) <- get
  let (o,s1') = runState m s1
  put (s1',s2)
  return o

Note the type:

GHCi> :t processingFst randomInputS
processingFst randomInputS :: State (StdGen, b) TurnstileInput

processingFst "converts" a State monad (in this case with state type StdGen) to another State monad (in this case with state type (StdGen, b), where b can be any type, even a TurstileState).

Exercises
  1. Implement processingSnd.
  2. Modify randomTurnS to use processingFst and processingSnd.

Note how randomTurnS is no longer directly involved in the details of the state management, and its business logic is much more apparent.

Generic Subcomponent Processing[edit | edit source]

We can see that processingFst and processingSnd are very similar. They both extract a subcomponent of a combined state, runState on that subcomponent, then update the combined state with the new value of the subcomponent.

Let's combine them into a single generic subcomponent processing function. To do this, we could pass in separate parameters, one of type (cmb -> sub) (a function that extracts a subcomponent from a combined state value), and another of type (cmb -> sub -> cmb) (a function that, given a combined value and a new value for a subcomponent, returns the revised combined value with the updated subcomponent). However, it's a bit neater to package these two functions together in a type which we'll call Lens:

data Lens cmb sub = Lens
  { view :: cmb -> sub,
    set  :: cmb -> sub -> cmb
  }

We can provide specific lenses onto the fst and snd elements in a pair:

fstL :: Lens (a,b) a
fstL = Lens fst (\(_,y) x -> (x,y))

sndL :: Lens (a,b) b
sndL = Lens snd (\(x,_) y -> (x,y))

So now:

GHCi> view fstL ("fred", 5)
"fred"
GHCi> set fstL ("fred", 5) "sue"
("sue",5)

Note

Lenses that are more sophisticated and powerful are described later, but it's also harder to understand how they work. Our simple lenses are sufficient for now, but you might want to update the random turnstile code to use "proper lenses" later on.


We can now replace processingFst and processingSnd with our generic function.

Exercises
  1. Implement processing that takes a Lens cmb sub parameter, and "converts" a State sub to a State cmb.
  2. Rewrite randomTurnS using the processing function (and fstL and sndL).

Our final random turnstile code is neater, with three separate logical functions segregated:

  • state management (now in a single processing utility function, which can be reused elsewhere);
  • subcomponent accessing and update (using Lens. We could use a standard package for this, which also provides many more lenses, automatic creation of lenses on custom data types, easy combining of lenses for deeply-nested subcomponents, etc); and
  • the "business logic" of the turnstile, which is now very apparent.

In our first implementation, all three of these were muddled together.

Let's give it a go:

GHCi> g <- newStdGen
GHCi> evalState (replicateM 10 randomTurnS) (g, Locked)
[Thank,Open,Tut,Thank,Thank,Open,Tut,Tut,Tut,Thank]

I'm not sure we'll sell many of them, though.

Notes

  1. Alternatively, we could make TurnstileInput an instance of Uniform, but this code seems easier.