Trying to learn Haskell I'm implementing a Quarto game in Haskell. I have already implemented the game in Python as an exercise in a course I took last year where the idea was to implement the game together with three different "AI" players, a random player, a novice player and a minimax player. The piece logic and board logic is quite straight forward to implement, but I have come to the point where I need to implement the players and I'm wondering how to best design the players so that the game logic doesn't need to know anything about the specific players, but still allowing them to use different monads.
The problem is that each player needs different monads, the random player needs to work in either a State monad or a RandomState monad. The novice player will probably also need some form of state and the minimax player could use either state or be pure(this would make it much slower and a bit trickier to implement, but it can be done) in addition i would like a "human" player which will need to work in the IO monad to get input from a human. One easy solution is to just put everything in the IO monad, but I feel that it is somewhat making the individual design harder and forcing the design of each player to have to deal with more than they should have to.
My initial thought would be something like:
class QuartoPlayer where
place :: (Monad m) => QuartoPiece -> QuartoBoard -> m (Int, Int)
nextPiece :: (Monad m) => QuartoBoard -> [QuartoPiece] -> m QuartoPiece
I don't know if this will work as I have not tried it, but I would like some input if I'm headed in the right direction and if the design makes sense in Haskell.
There are two parts to what is going on here. First is how to combine the several different types of monad to run at the same time — and as has been pointed out this can be done with monad transformers — and the second is allowing each of your player types access only to the monads they need. The answer to this latter problem is type classes.
So firstly, lets examine monad transformers. A monad transformer is like a monad with an additional 'internal' monad. If this internal monad is the Identity monad (which basically does nothing) then the behaviour is just like the regular monad. For this reason monad are usually implemented as transformers and wrapped in Identity to export a normal monad. Transformer versions of monads usually append T to the end of the type, so the state monad transformer is called StateT. The only difference in the types is in the addition of the inner monad, State s a
vs Monad m => StateT s m a
. So for an example, an IO monad with an attached list of integers as state could have type StateT [Int] IO
.
Two more points are needed to properly use the transformers. First is that to effect the inner monad, you use the lift
function (which any existing monad transformer will have defined). Each call of lift moves you one down the stack of transformers. liftIO
is a special shortcut for when the IO monad is at the bottom of the stack. (And it can't be anywhere else as there is no IO transformer as you would expect.) So we could make a function that pops the head of our int list from the state part and prints it using the IO part:
popAndPrint :: StateT [Int] IO Int
popAndPrint = do
(x:xs) <- get
liftIO $ print x
put xs
return x
The second point is that you need transformer versions of the running functions, one for each monad transformer in the stack. So in this case to demonstrate the effect in GHCi we need
> runStateT popAndPrint [1,2,3]
1
(1,[2,3])
If we wrapped this in an Error monad, we'd need to call runErrorT $ runStateT popAndPrint [1,2,3]
and so on.
That is a quick fire intro to monad transformers, there is plenty more available online.
However, for you this is only half of the story, as ideally you want a separation between which monads your different player types can use. The transformer approach seems to give you everything and you don't really want to give all the players access to IO just because one needs it. So how to proceed?
Each different type of player needs access to a different part of the transformer stack. So make a type class for each player that exposes only what that player needs. Each one could go in a different file. For example:
-- IOPlayer.hs
class IOPlayerMonad a where
getMove :: IO Move
doSomethingWithIOPLayer :: IOPlayerMonad m => m ()
doSomethingWithIOPLayer = ...
-- StatePlayer.hs
class StatePlayerMonad s a where
get :: Monad m => StateT s m s
put :: Monad m => s -> StateT s m ()
doSomethingWithStatePlayer :: StatePlayerMonad s m => m ()
doSomethingWithStatePlayer = ...
-- main.hs
instance IOPlayerMonad (StateT [Int] IO) where
getMove = liftIO getMoveIO
instance StatePlayerMonad s (StateT [Int] IO) where
get' = get
put' = put
This gives you control over what part of the app can access what from the overall state, and this control all happens in one file. Each individual part gets to define its interface and logic quite apart from specific implementation of the main state.
PS, you may need these at the top:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
import Control.Monad.Trans.State
import Control.Monad.IO.Class
import Control.Monad
-
There has been some confusion about whether you can do it this way and still have a common interface to all players. I maintain that you can. Haskell is not object oriented and so we need to do a little bit of the dispatch plumbing ourselves, but the results are just as powerful and you get better control of the details and can still achieve full encapsulation. To better show this I have included a fully working toy example.
Here we see that the Play
class provides a single interface to a number of different player types, each with their logic in their own file and only seeing a specific interface onto the transformer stack. This interface is controlled in the Play module, and the game logic need use only this interface.
Adding a new player involves making a new file for them, designing the interface they require, adding this to the AppMonad, and wiring it up with a new tag in the Player type.
Note that all players get access to the board via the AppMonadClass class, which could be expanded to include any required common interface elements.
-- Common.hs --
data Board = Board
data Move = Move
data Player = IOPlayer | StackPlayer Int
class Monad m => AppMonadClass m where
board :: m Board
class Monad m => Play m where
play :: Player -> m Move
-- IOPlayer.hs --
import Common
class AppMonadClass m => IOPLayerMonad m where
doIO :: IO a -> m a
play1 :: IOPLayerMonad m => m Move
play1 = do
b <- board
move <- doIO (return Move)
return move
-- StackPlayer.hs --
import Common
class AppMonadClass m => StackPlayerMonad s m | m -> s where
pop :: Monad m => m s
peak :: Monad m => m s
push :: Monad m => s -> m ()
play2 :: (StackPlayerMonad Int m) => Int -> m Move
play2 x = do
b <- board
x <- peak
push x
return Move
-- Play.hs --
import Common
import IOPLayer
import StackPlayer
type AppMonad = StateT [Int] (StateT Board IO)
instance AppMonadClass AppMonad where
board = return Board
instance StackPlayerMonad Int AppMonad where
pop = do (x:xs) <- get; put xs; return x;
peak = do (x:xs) <- get; return x;
push x = do (xs) <- get; put (x:xs);
instance IOPLayerMonad AppMonad where
doIO = liftIO
instance Play AppMonad where
play IOPlayer = play1
play (StackPlayer x) = play2 x
-- GameLogic.hs
import Play
updateBoard :: Move -> Board -> Board
updateBoard _ = id
players :: [Player]
players = [IOPlayer, StackPlayer 4]
oneTurn :: Player -> AppMonad ()
oneTurn p = do
move <- play p
oldBoard <- lift get
newBoard <- return $ updateBoard move oldBoard
lift $ put newBoard
liftIO $ print newBoard
oneRound :: AppMonad [()]
oneRound = forM players $ (\player -> oneTurn player)
loop :: AppMonad ()
loop = forever oneRound
main = evalStateT (evalStateT loop [1,2,3]) Board
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With