Monad Wars – 3: Command line actions

After the last post, we have parser actions that can recognise an
integer or an item of merchandise. Now we need to be able to process a
command, like “jet bronx” or “buy 4 lambdas”. Let's start off with this
basis:

 > parseCommand = parseMap commandMap
 > commandMap = getPrefixMap [ >            ( "buy",  cmdBuy  ), >            ( "sell", cmdSell ), >            ( "jet",  cmdJet ), >            ( "quit", cmdQuit ) >          ]

Now, what we roughly want to do is:

  • Tokenise the line
  • Check if the first token maps to a command
  • Check if the rest of the tokens can be handled by that command.
  • The result (if applicable) is a function that maps an original GameState into a new state.

We might come up with something like this:

 > parseLine s = do let (cmd:pars) = tokenizeLine s >                  c  <- parseCommand cmd >                  c' <- c pars >                  return c'

But I'd promised that we'd check if the parsing worked! Can you see any checks like that above?

Possibly Maybe

If we check the type of parseCommand, we'll notice it returns a Maybe

 parseCommand :: [Char]     -> Maybe ([String] -> Maybe (GameState -> Maybe GameState))

This means that the do expression starts with a Maybe (we don't count the let expression) and so is in
the Maybe monad. If any of the sequence fails, parseLine will return
Nothing, without us having to specify anything! This is quite cute and,
once you get used to it, rather intuitive (the definition above fell
naturally out of my text editor).

Here's another example of Maybe – parsing the expressions like “buy
4 curry” or “sell 10 stm”. First of all, we notice that cmdBuy and
cmdSell both have the same form, so we'll share the code in a common
parser called cmdMerchandise.

 > cmdBuy  = cmdMerchandise  doBuy > cmdSell = cmdMerchandise  doSell

This parser looks at the first 2 parameters, and tries to parse them respectively as an Int or an item of Merchandise.

If either of the parses fails, it will magically return Nothing.

If it succeeds, it will return the result of, for example doBuy 10 lambdas.
(The result is of course a function that takes a GameState in input,
and returns a GameState that is the result of having bought 10 lambdas.
Very meta.)

 > cmdMerchandise f (n:m:_) = do n' <- parseInt n >                               m' <- parseMerchandise m >                               Just $ f n' m' > cmdMerchandise _ _       = Nothing

Let's play

This is getting a little abstract if we can't test it. Right now our
GameState record doesn't have a “list of merchandise” structure, so
let's keep it simple for the sake of argument and add a debug string
instead.

 > data GameState = GameState { >        turn     :: Integer, >        score    :: Integer, >        location :: Location, >        debug    :: String >    } deriving Show > > type Location = Int

(and add a debug = ““ to the startState declaration.)

We'll make the doBuy and doSell functions just modify the debug string:

 > doBuy  n m gs = return gs {  >     debug = "You bought " ++  >             (show n) ++ " " ++ >             (name m) >     }
 > doSell  n m gs = return gs {  >     debug = "You sold " ++  >             (show n) ++ " " ++ >             (name m) >     }

OK, we could factor these out as an exercise, but we'll be replacing
them soon. Now, what we really want to do is to test it! I'll look at
plugging this into the prompt structure next time, for now let's just
create a test function. This just takes a GameState and a line, and
returns the new state if it all worked out.

 > test gs s = do c <- parseLine s >                c gs

We can play with this to see if it worked:

 *Main> test startState "sell 3 la" Just (GameState {turn = 1, score = 0, location = 0,                   debug = "You sold 3 Lambdas"}) *Main> test startState "panic" Nothing

The other actions are similar. We'll use the record mutators modScore and nextTurn that we saw last time.

 > -- Just a stub: We'll probably want to set an "endflag" or similar. > cmdQuit _ = Just doQuit > doQuit gs = return $ modScore (-10) gs {  >                         debug = "Quitter!" } >  > cmdJet (n:_) = do n' <- parseInt n >                   Just $ doJet n' > cmdJet _     = Nothing >  > doJet n gs | n == location gs >               = return $ gs {  >                     debug = "You are already in location "  >                              ++ show n }  >            | otherwise  >               -- Jetting increments the turn counter >               = return $ nextTurn gs { >                     location = n, >                     debug    = "You have moved to "  >                                  ++ show n } 

And we can now test the remaining actions:

 *Main> test startState "jet" Nothing *Main> test startState "jet 1" Just (GameState {turn = 2, score = 0, location = 1,                   debug = "You have moved to 1"}) *Main> test startState "jet 0" Just (GameState {turn = 1, score = 0, location = 0,                   debug = "You are already in location 0"}) *Main> test startState "qu" Just (GameState {turn = 1, score = -10, location = 0,                   debug = "Quitter!"})

Next time around, we'll plug these actions into our prompt, and we'll work on representing the game state