In this refactor you'll work on the GameState
module. Your mission is to refactor the code to use the State
monad. Before doing that you will refactor the code to make isolated changes in the state (I'm explaining this soon, bear with me). To summarize, this refactor has two steps:
- step 1: make isolated changes in the state as pure functions
- step 2: realizing the emerging pattern as a monad.
Ok, enough buzzwords. In the GameState
module you have functions like newApple
or nextHead
that take the BoardInfo
and parts of the GameState
and produce a result. This result is used in the move
function to update the parts of the state that changes. For example, newApple
produces a (Point, StdGen)
. If the snake is eating the apple, this function is responsible of producing a Point
for the new apple and a StdGen
that will be used for the next apple. This has two inconveniences:
- The function
move
has too many responsibilities, it moves the snake body, checks if the apple is there, produces a new apple, etc... - Every time we call any of this function, the caller must remember to update the state. In other words, we've delegated the
newApple
's responsibility to thenewApple
's caller.
Of course, not always is clear who is responsible for what... that's software development! In this case (for the sake of learning) we are going to modify the code so we pass state updates to the callee instead of the caller. Let's split the move
function using a very common pattern!
In this step we are going to refactor the structure of our functions so all of them follow a pattern. Improving code quality.
Look at the type of move
, it is BoardInfo -> GameState -> (GameState, [RenderMessage])
. Meaning that, it takes the GameState
and produces a pair consisting in the new state and a result, which in this case is [RenderMessage]
. We can apply the same trick to every function: take the GameState
and produce a pair with the new state and a result.
- Change the function
makeRandomPoint
from typeBoardInfo -> StdGen -> (Point, StdGen)
to typeBoardInfo -> GameState -> (Point, GameState)
. The function should return the random point and a new state with therandomGen
field updated. - Fix any compiler error
- Change the function
newApple
from typeBoardInfo -> GameState -> (Point, StdGen)
to typeBoardInfo -> GameState -> (Point, GameState)
. The function should return the new apple point and a new state with theapplePosition
field updated. - Fix any compiler error
- Create function
extendSnake :: Point -> BoardInfo -> GameState -> (RenderState.DeltaBoard, GameState)
such that given apoint
representing the new position of the head, then it extends the current snake appending the new head. It returns theGameState
with the fieldsnakeSeq
modified andRenderState.DeltaBoard
with the changes we need to send to theBoard
in theRenderState
. In other words, this function will be called when the snake is eating the apple. - Create function
displaceSnake :: Point -> BoardInfo -> GameState -> (RenderState.DeltaBoard, GameState)
such that given apoint
representing the new position of the head, then it displaces the current snake by appending the new head and removing the tail. It returns theGameState
with the fieldsnakeSeq
modified andRenderState.DeltaBoard
with the changes we need to send to theBoard
in theRenderState
. In other words, this function will be called when the snake is not eating the apple.
In this step we are going to define the State
monad and a justification of it based on the previous pattern we've introduced. We are covering the following topics:
- There is a clear pattern introduced in Step 1
- The previous refactor has some problems
- What is the
State
monad? - How does it help to make better code?
We are verging to the part in which we use monads. The previous refactor has been difficult, but hopefully the function move
now is clearer than before (if it isn't check the solution). On the way you've probably spotted a pattern: we are using always functions with almost the same structure: some_function :: some_arguments -> GameState -> (Result, GameState)
(forget about BoardInfo
and other arguments and let's focus on the last part of the signature.). These funtions always work the same way: they take some arguments, the game state and produce the updated game state and a result (a Point
, a DeltaBoard
, etc...).
Probably, in the previous step you've found yourself manually unpacking calls to these functions to use the updated state elsewhere. For example, I got something similar to this:
let (delta, game_state2) = extendSnake newHead board_info game_state1
(point, game_state3) = newApple board_info game_state2
This manual handling of states is clumsy and error prone. Very easily you can forget to pass the updated state to the next function (if you don't believe me check the commit history, it has happened to me). The State
monad is the solution for this. Before talking about the State
monad you should notice:
- The
State
monad is just an example (or an monad instance). It isn't THE definition of monad. You'll learn more monad examples in this challenge. - The
State
monad does not provide more functionality!! It's just a convenient interface for the problem we've seen before. This is a key concept: Monads do not provide any extra functionality, they are just a convenient interface for many different patterns.
Now, let's go to the definition. The State
monad is nothing else than a function from a state to a pair of a result and the updated state type State a = SomeState -> (a, SomeState)
(the actual implementation is a little bit different). I'd recommend monday morning haskell blog as a reference for learning monads in depth. Here we are providing a shallow explanation. A very important concept to understand when learning the State
monad is that you are not handling a piece of data, you are defining a function. Maybe, a better name would have been the StateTransformation
monad. I know this sounds abstract right now, but keep this in mind: the state monad defines a transformation on a piece of data that will be provided later.
Now, how does the state monad help with the implementation? Essentialy, it applies state transformation automatically. Following the previous example:
# Without state monad you have this
let (delta, game_state2) = extendSnake newHead board_info game_state1
(point, game_state3) = newApple board_info game_state2
in ...
# With state monad this
extendAndCreateNewApple newHead board_info = extendSnake newHead board_info >> newApple board_info >> ...
Wait what? Did the state handling disappear?. Yes!, that's the magic of the state monad. You defined small functions modifying the state and then you chain them together using operators like >>
, >>=
or >=>
. Also, Haskell provides syntactic sugar for those operators in the form of the do
-notation. You should read about it.
This will be a hard refactor. Keep that in mind, you'll need some time to get used to. Also remember that the state monad defines a transformation on a piece of data that will be provided later. This will be useful when using functions like get
which seem to magically produce a piece of data out of nowhere.
- Use the following imports:
import Control.Monad.Trans.State.Strict (State, get, put, modify, gets, runState)
- First define a type synonym
type GameStep a = State GameState a
. You'll need to importControl.Monad.State.Strict
. - Change functions so instead of having type
GameState -> (a, GameState)
they haveGameStep a
. Also, rename functionmove
tostep
and create a functionmove
which actually runs theState
monad. You should refactor at least the following functions to have the given types:makeRandomPoint :: BoardInfo -> GameStep Point
newApple :: BoardInfo -> GameStep Point
extendSnake :: Point -> BoardInfo -> GameStep DeltaBoard
displaceSnake :: Point -> BoardInfo -> GameStep DeltaBoard
step :: BoardInfo -> GameStep [Board.RenderMessage]
. This is the functionmove
renamedmove :: BoardInfo -> GameState -> ([Board.RenderMessage], GameState)
. This is a new functionmove
which is defined in terms ofrunState
andstep
Good luck!