< Part III: How Haskell Monads are Like a Muppet Part: V (coming soon) >
Finally, with the forth part of the tutorial, I get to describe something that will make my PHP-coding reader want to use Haskell: lazy evaluation.
But first, let’s go back to two different ways I’ve approached the problem of evaluating a keyboard input from a user in our rogue-like RPG game. In tutorial #2, I created a function called move that used guards to discern what action to take based on a given input. Here is move:
move :: Char -> String
| a == 'w' = "UP!"
| a == 's' = "DOWN!"
| a == 'a' = "LEFT!"
| a == 'd' = "RIGHT!"
| otherwise = "HANG AROUND AND DO NOTHING!"
In tutorial #3 I switched to a series of if / else if / else statements.
char <- getChar
if char == 'q' then putStrLn "Thanks for Playing!"
else if char == 'w' then up y >> output x >> output y
else if char == 's' then down y >> output x >> output y
else if char == 'a' then down x >> output x >> output y
else if char == 'd' then up x >> output x >> output y
else putStrLn "you did not type a correct entry")
This version had the additional side effect of requiring a manual escape of the compiler program in order to quit (something that is easily fixable, but still). Now I have a new function ‘dashboard’ that uses lazy evaluation to compute values based on a given input. Without beating around the bush, here it is:
dashboard :: Char -> MVar Int -> MVar Int -> IO ()
dashboard 'q' _ _ = exit "Thanks for Playing!" // stop the program
dashboard 'w' x y = up y >> putStr " Coordinate: ( x = " >> output x >> putStr " , y = " >> output y >> putStr ")."
dashboard 's' x y = down y >> putStr " Coordinate: ( x = " >> output x >> putStr ", y = " >> output y >> putStr ")."
dashboard 'a' x y = down x >> putStr " Coordinate: ( x = " >> output x >> putStr ", y = " >> output y >> putStr ")."
dashboard 'd' x y = up x >> putStr " Coordinate: ( x = " >> output x >> putStr ", y = " >> output y >> putStr ")."
dashboard w x y = putStr " Coordinate: ( x = " >> output x >> putStr ", y = " >> output y >> putStr ")." // keep the Coordinate values the same
So, let’s think about our problem for a second. So far, we have been focussed on getting our character to move around a map. But what if we want to do something completely different, like say show an inventory list if the user hits ‘i’? Or maybe cast a spell? We could have a bunch of if – then statements or a whole bunch of guards, but it all could end up being a mess and impossible to read.
Fortunately, Haskell let’s you specify particular actions based on the constructors (entry values) for your function. In the case above, dashboard accepts 3 values — one CHAR and two MVar Ints (ie. a mutable variable that has to be an integer — see the previous tutorial). If the dashboard receives a ‘q’ for a value, it will say ‘thanks for playing’ and quit the program (the two ‘_’s mean it doesn’t matter what the values are that follow it). The next entries do what we’ve come to expect -> change the value of the MVar based on the movement and then output the current coordinate’s status. The last statement is a catch-all, asking Haskell to output the current spot.
Why might this way of doing things be better than using (say) guards? Well, the first reason is that it gets to the point. if the user hits ‘q’, Haskell will not attempt to evaluate any of the other function calls. Admittedly, this function isn’t really helped performance-wise by lazy evaluation -> but what if moving in a certain direction (in the spot where a monster was) involved a long series of recursive computations. We would not want Haskell to be using up its resources trying to figure out what it’s supposed to do when all the user wants to do is quit.
However, there’s still alot that could be added here. What if we want to do something when the user hits ‘s’ and y is equal to zero. Then we could do something like this:
dashboard 's' 0 y putStrLn "You are blocked and can go no further." >> putStr " Coordinate: ( x = " >> output x >> putStr ", y = " >> output y >> putStr ")."
Again, this can have really great performance benefits for your program. If dashboard gets an ‘s’ and a ‘0’, it will look no further and do the easy job of outputting the coordinate as is, rather than attempting to go through all kinds of irrelevant processes. UPDATE: Well, actually, this is all wrong. Because ‘0’ is not an MVAR. Mind you, there is a way to get the function to accept two Ints instead and convert the MVAR to an Int for our purposes. That’ll be another tutorial though.
The last benefit is that it gives a nice human-readable understanding of what happens when the function receives what, without a whole range of complex nested elements.
The downside, of course, is that because Haskell is type-safe, you must give this function a CHAR (not a Maybe Char, or a String) and two MVAR Ints (not an MVAR String or ordinary Int) or you will get a type error. Things could also get more confusing if you decide that you want to return an IO (String) or other value instead of just outputting something to the screen. But handling that problem might be for another tutorial.
I should, again, remind you that my up-to-date work on the rogue-like game called rasghiosse is available in a patch-tag repository. Please feel free to add to it as you wish.