Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p>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.</p> <p>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, <code>State s a</code> vs <code>Monad m =&gt; StateT s m a</code>. So for an example, an IO monad with an attached list of integers as state could have type <code>StateT [Int] IO</code>. </p> <p>Two more points are needed to properly use the transformers. First is that to effect the inner monad, you use the <code>lift</code> function (which any existing monad transformer will have defined). Each call of lift moves you one down the stack of transformers. <code>liftIO</code> 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:</p> <pre><code>popAndPrint :: StateT [Int] IO Int popAndPrint = do (x:xs) &lt;- get liftIO $ print x put xs return x </code></pre> <p>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</p> <pre><code>&gt; runStateT popAndPrint [1,2,3] 1 (1,[2,3]) </code></pre> <p>If we wrapped this in an Error monad, we'd need to call <code>runErrorT $ runStateT popAndPrint [1,2,3]</code> and so on.</p> <p>That is a quick fire intro to monad transformers, there is plenty more available online.</p> <p>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?</p> <p>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:</p> <pre><code>-- IOPlayer.hs class IOPlayerMonad a where getMove :: IO Move doSomethingWithIOPLayer :: IOPlayerMonad m =&gt; m () doSomethingWithIOPLayer = ... -- StatePlayer.hs class StatePlayerMonad s a where get :: Monad m =&gt; StateT s m s put :: Monad m =&gt; s -&gt; StateT s m () doSomethingWithStatePlayer :: StatePlayerMonad s m =&gt; m () doSomethingWithStatePlayer = ... -- main.hs instance IOPlayerMonad (StateT [Int] IO) where getMove = liftIO getMoveIO instance StatePlayerMonad s (StateT [Int] IO) where get' = get put' = put </code></pre> <p>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.</p> <p>PS, you may need these at the top:</p> <pre><code>{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} import Control.Monad.Trans.State import Control.Monad.IO.Class import Control.Monad </code></pre> <p>-</p> <h1>UPDATE</h1> <p>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.</p> <p>Here we see that the <code>Play</code> 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.</p> <p>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.</p> <p>Note that all players get access to the board via the AppMonadClass class, which could be expanded to include any required common interface elements.</p> <pre><code>-- Common.hs -- data Board = Board data Move = Move data Player = IOPlayer | StackPlayer Int class Monad m =&gt; AppMonadClass m where board :: m Board class Monad m =&gt; Play m where play :: Player -&gt; m Move -- IOPlayer.hs -- import Common class AppMonadClass m =&gt; IOPLayerMonad m where doIO :: IO a -&gt; m a play1 :: IOPLayerMonad m =&gt; m Move play1 = do b &lt;- board move &lt;- doIO (return Move) return move -- StackPlayer.hs -- import Common class AppMonadClass m =&gt; StackPlayerMonad s m | m -&gt; s where pop :: Monad m =&gt; m s peak :: Monad m =&gt; m s push :: Monad m =&gt; s -&gt; m () play2 :: (StackPlayerMonad Int m) =&gt; Int -&gt; m Move play2 x = do b &lt;- board x &lt;- 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) &lt;- get; put xs; return x; peak = do (x:xs) &lt;- get; return x; push x = do (xs) &lt;- 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 -&gt; Board -&gt; Board updateBoard _ = id players :: [Player] players = [IOPlayer, StackPlayer 4] oneTurn :: Player -&gt; AppMonad () oneTurn p = do move &lt;- play p oldBoard &lt;- lift get newBoard &lt;- return $ updateBoard move oldBoard lift $ put newBoard liftIO $ print newBoard oneRound :: AppMonad [()] oneRound = forM players $ (\player -&gt; oneTurn player) loop :: AppMonad () loop = forever oneRound main = evalStateT (evalStateT loop [1,2,3]) Board </code></pre>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload