Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell design encompassing several monads

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.

like image 408
Nordmoen Avatar asked Sep 18 '13 07:09

Nordmoen


1 Answers

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

-

UPDATE

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
like image 128
Vic Smith Avatar answered Oct 16 '22 13:10

Vic Smith