Ryan Ingram wrote:
I've been trying to implement a few rules-driven board/card games in Haskell
and I always run into the ugly problem of "how do I get user input"?

The usual technique is to embed the game in the IO Monad:

The problem with this approach is that now arbitrary IO computations are
expressible as part of a game action, which makes it much harder to
implement things like replay, undo, and especially testing!

The goal was to be able to write code like this:

] takeTurn :: Player -> Game ()
] takeTurn player = do
]     piece  <- action (ChoosePiece player)
]     attack <- action (ChooseAttack player piece)
]     bonusTurn <- executeAttack piece attack
]     when bonusTurn $ takeTurn player

but be able to script the code for testing, allow undo, automatically
be able to save replays, etc.

While thinking about this problem earlier this week, I came up with the
following solution:

class Monad m => MonadPrompt p m | m -> p where
   prompt :: p a -> m a

"prompt" is an action that takes a prompt type and gives you a result.

A simple example:
] prompt [1,3,5] :: MonadPrompt [] m => m Int

This prompt would ask for someone to pick a value from the list and return
it.
This would be somewhat useful on its own; you could implement a "choose"
function that picked randomly from a list of options and gave
non-deterministic (or even exhaustive) testing, but on its own this wouldn't
be much better than the list monad.
[...]
data Prompt (p :: * -> *) :: (* -> *) where
    PromptDone :: result -> Prompt p result
    -- a is the type needed to continue the computation
    Prompt :: p a -> (a -> Prompt p result) -> Prompt p result

Intuitively, a (Prompt p result) either gives you an immediate result
(PromptDone), or gives you a prompt which you need to reply to in order to
continue the computation.

This type is a MonadPrompt:

instance Functor (Prompt p) where
   fmap f (PromptDone r) = PromptDone (f r)
   fmap f (Prompt p cont) = Prompt p (fmap f . cont)

instance Monad (Prompt p) where
   return = PromptDone
   PromptDone r  >>= f = f r
   Prompt p cont >>= f = Prompt p ((>>= f) . cont)

instance MonadPrompt p (Prompt p) where
   prompt p = Prompt p return

Marvelous!

Basically, by making the continuation (a -> Prompt p result) explicit, we have the flexibility to acquire the value a differently, like through user input or a replay script. The popular continuations for implementing web applications in Lisp/Scheme do the same thing.

A slightly different point of view is that you use a term implementation for your monad, at least for the interesting primitive effects like

  choosePiece   :: Player -> Game Piece
  chooseAttack  :: Player -> Piece -> Game Attack

By using constructors for them, you have the flexibility to write different interpreters for Game a , like

  play   :: Game a -> IO a
  replay :: Game a -> GameScript -> a

with the semantics

  play (choosePiece pl >>= f) = do
     putStrLn "Player " ++ show pl ++ ", choose your piece:"
     play f . read =<< getLine

  replay (choosePiece pl >>= f) (Piece pl' piece:xs)
     | pl == pl' = replay (f piece) xs

Just for the record, the most general term implementation is presented here

  Chuan-kai Lin. Programming Monads Operationally with Unimo.
  http://web.cecs.pdx.edu/~cklin/papers/unimo-143.pdf


Btw, the web framework WASH/CGI for Haskell uses some kind of prompt monad, too.

  Peter Thiemann. An Embedded Domain-Specific Language for
  Type-Safe Server-Side Web-Scripting.
  http://www.informatik.uni-freiburg.de/~thiemann/WASH/draft.pdf

Here, the server replays parts of the CGI monad when the user submits a form i.e. answers to a prompt.


Regards,
apfelmus

_______________________________________________
Haskell-Cafe mailing list
Haskell-Cafe@haskell.org
http://www.haskell.org/mailman/listinfo/haskell-cafe

Reply via email to