Part 5: The State Monad

In the previous part of this series, we learned about the Reader and Writer monads. These gave us a new perspective on Haskell. They showed that in fact we can have global variables of some sort; we just need to encode them in the type signature somehow, and this is what monads are for! In this part, we’ll explore the State monad, which combines some of the functionality of both these concepts.

Once you understand State, you’ll be well on your way to being a Haskell pro! Download our Production Checklist to learn some of the cool ways you can apply your newfound skills!

Motivating Example: Monopoly

For this part, we’ll use a simple model for a Monopoly-like game. The main object is the GameState data type containing several important pieces of information.

data GameState = GameState
  { players :: [Player]
  , chanceDeck :: [GameCard]
  , properties :: Map Property PropertyState
  , piecePositions :: Map Player Property 
  . generator :: StdGen }

data PropertyState = Unowned | Owned Player

data ChanceCard = …
data Player = …
data BoardPostion = …
data GameAction = ...

Let’s think at a high level about how some of our game functions would work. We could, for instance, have a function for rolling the dice. This would output a number and alter our game’s number generator. We would then make a move based on the dice output and the current player. This would change the piece positions in the board state as well as leaving us with an output action to resolve. The output action would be something like drawing a card, or taking action on a property.

Buying a property also changes the board’s state. Drawing a chance card would update the state of the deck while returning us a GameCard to resolve. We see a common pattern here among the different actions. Almost all of them will update GameState in some way, and some of them will have an additional piece of output we’ll want to use.

The State Monad

This is exactly the situation the State monad deals with. The State monad wraps computations in the context of reading and modifying a global state object. This context chains two operations together in an intuitive way. First, it determines what the state should be after the first operation. Then, it resolves the second operation with the new state.

It is parameterized by a single type parameter s, the state type in use. So just like the Reader has a single type we read from, the State has a single type we can both read from and write to.

The two main functions we’ll use within the State monad with are get and put. They do exactly what you expect they’d do. The get function works much like the ask function of the reader monad, retrieving our state value. Meanwhile, put works similarly to tell in the Writer monad, where we’ll pass an updated state. Finally we observe there will still be a final return type on each expression in State, just as there is in any other monad. Thus our different function types will look like this for a return type of a:

State GameState a

Our Monopoly Functions

Now we can examine some of the different functions mentioned above and determine their types. We have for instance, rolling the dice:

rollDice :: State GameState Int
rollDice = do
  currentState <- get
  let gen = generator currentState
  let (d1, gen') = randomR (1,6) gen
  let (d2, gen'') = randomR (1,6) gen'
  put (currentState { generator = gen'' } )
  return (d1 + d2)

This outputs an Int to us, and modifies the random number generator stored in our state! Now we also have the function making a move:

movePiece :: Player -> Int -> State GameState Property
movePiece player roll = do
  currentState <- get
  let currentPositions = piecePositions currentState
  let currentPos = fromJust (M.lookup player currentPositions)
  let next = nextProperty currentPos roll
  let newMap = M.insert player next currentPositions
  put (currentState { piecePositions = newMap } ) 
  return next

nextProperty :: Property -> Int -> Property

This will give us the output of the new property we landed on, while also modifying the board with our new position of our piece. Based on the resulting position, we might take different actions, like drawing a chance card:

drawChance :: State GameState ChanceCard
drawChance = do
  currentState <- get
  let (fstCard : deck) = chanceDeck currentState
  put (currentState { chanceDeck = deck } )
  return fstCard

As we said above, this will modify the pile of available cards in the chance pile. There are other stateful functions we could describe, such as resolving a property purchase, or paying rent to another player. These would also exist within the state monad.

buyProperty :: Player -> Property -> State GameState ()

payRent :: Player -> Property -> State GameState ()

So finally, we can combine all these functions together with do-syntax, and it actually looks quite clean! We don’t need to worry about the side effects. The different monadic functions handle them. Here’s a sample of what your function might look like to play one turn of the game:

resolveTurn :: State GameState ()
resolveTurn = do
  currentState <- get
  let playerToMove = currentPlayer currentState 
  roll <- rollDice
  newProperty <- movePiece playerToMove roll
  action <- actionForProperty playerToMove newProperty
  resolveAction action
  return ()

Obviously, we haven’t described all these functions, but the general idea should be clear. They would all exist within the state monad.

State, IO, and Other Languages

When thinking about Haskell, it is often seen as a restriction that we can’t have global variables like you could with Java class variables. However, we see now this isn’t true. We could have a data type with exactly the same functionality as a Java class. We would just have many functions that can modify the global state of the class object using the State monad.

The difference is in Haskell we simply put a label on these types of functions. We don’t allow it to happen for free. We want to know when side effects can potentially happen, because knowing when they can happen makes our code easier to reason about. In a Java class, many of the methods won’t actually need to modify the state. But they could, which makes it harder to debug them. In Haskell we can simply make these pure functions, and our code will be simpler.

IO is the same way. It’s not like we can’t perform IO in Haskell. Instead, we want to label the areas where we can, to increase our certainty about the areas where we don’t need to. When we know part of our code cannot communicate with the outside world, we can be far more certain of its behavior.


That wraps it up for the State monad! Now that we know all these different monad constructs, you might be wondering how we can combine them. What if there was some part of our state that we wanted to be able to modify (using the State monad), but then there was another part that was read-only. How can we get multiple monadic capabilities at the same time? To learn to answer, head to part 6! In the penultimate section of this series, we’ll discuss monad transformers. This concept will allow us to compose several monads together into a single monad!

Now that you’re starting to understand monads, you can really pick up some steam on learning some useful libraries for important tasks. Download our Production Checklist for some examples of libraries that you can learn!