I have a module Game
that defines a method play like this play :: Board -> Move - > Board
.
I want to use State Monad in another module called Playing
that imports the Game
module so that I can call play
from there on a loop until the Board
reaches a certain state.
I want to call the method with play with the Board
that I get from the State Monad and then update the State Monad value with the Board
returned by play
.
So while the loop is going on I want to receive moves to apply to the play
method and my current state.
But I'm quite lost as in how achieving this in a way that the module Game
has no idea that I'm using a State Monad.
I have been looking at quite a few tutorials and examples (like this, this, this, etc) and I feel like I understand the way in which State Monad it's applied there but apparently not well enough as to abstract it to this particular implementation.
playing :: IO ()
playing = do
putStr $ "The board looks like:"
board <- get
putStr $ showBoard board
putStr $ "Indicate a move:"
move <- getLine
if validMove move then do
newBoard <- play board (getMove move)
if gameEnded newBoard then do
putStr $ "You win!" --stop the execution
else do
put newBoard
else do
putStr $ "Invalid move"
I want playing
to be on a loop until it gets a particular Board
that means the game ended. And use the State Monad to send the current Board
to play
and to the other methods in the Game
module like gameEnded :: Board -> Bool
, showBoard :: Board -> String
and 'getMove :: String -> Move'.
Any help is welcome
If you swap the order of arguments to play
, you have a function of type:
Move -> Board -> Board
Which you can partially apply with a Move
to get one of type:
Board -> Board
You can convert this to an action on State
using modify :: (s -> s) -> State s ()
to modify the board:
playing :: Move -> State Board ()
playing move = modify (play move)
One solution here is monad transformers—sounds scarier than it is. You could use StateT
over IO
, the StateT
to store the game state and the IO
to prompt the user for moves. For example:
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State (evalStateT, gets, modify)
-- Get a move from the user.
getMove :: IO Move
getMove = do
line <- getLine
-- (Your implementation of parsing moves here.)
-- The initial state of the board.
initialBoard :: Board
initialBoard = -- ...
-- Whether the board represents a completed game.
boardDone :: Board -> Bool
boardDone board = -- ...
-- Main game loop.
gameLoop :: IO ()
gameLoop = evalStateT loop initialBoard
where
loop = do
move <- lift getMove
modify (play move)
done <- gets boardDone
if done then pure () else loop
You use lift
to convert a normal IO
action into a StateT Board IO
action, modify :: (Monad m) => (s -> s) -> StateT s m ()
to modify the state, and gets :: (Monad m) => (s -> a) -> StateT s m a
to read properties of the current state. loop
either tail-calls itself to continue playing, or else returns.
Using the structure and names in your edited question:
playing :: IO ()
playing = evalStateT loop initialBoard
where
loop :: StateT Board IO ()
loop = do
printBoard
move <- lift promptMove
modify (play move)
ended <- gets gameEnded
if ended
then lift $ putStrLn "You win!"
else loop
printBoard :: StateT Board IO ()
printBoard = do
lift $ putStrLn $ "The board looks like:"
board <- get
lift $ putStrLn $ showBoard board
promptMove :: IO Move
promptMove = do
putStr "Indicate a move: "
move <- getLine
if validMove move
then pure $ getMove move
else do
putStrLn "Invalid move."
promptMove