Monday, June 8, 2009

Messing with Haskell

This week I spent some time attempting to port the old BASIC game Hamurabi to Haskell. I'd picked up a print copy of Real World Haskell so I was looking for something to try out.

It's been a challenge, and I haven't gotten as far as I would have expected. I wish the Haskell documentation, whether online or provided with the installation, was put together better. I'd like to have ready access to a language reference, not just the auto-generated library documentation; I'd like to have lots of sample code snippets; and I'd like for it to be fully searchable.

For instance: in BASIC you can write INPUT A to read an integer from the keyboard. The closest equivalent in Haskell is read but it aborts the program with an exception if the user does not type in something that can be parsed correctly. Since I was attempting to match BASIC's behavior I needed it to be able to deal more gracefully with bad input. With Google I eventually found a thread somewhere addressing this problem.

So far my program is not ending up any smaller than the original BASIC version, which surprises me a bit. I'm having difficulty deciding the best way to structure the program. I've discovered a method that emulates BASIC's goto style pretty closely, but it doesn't seem like that's necessarily a step toward readability, because the flow of control is embedded throughout the program:


main = buyLand

buyLand =
[choose how much land to buy with stored food]
if bought land:
feedPeople
else:
sellLand

sellLand =
[choose how much land to sell for food]
feedPeople

feedPeople =
[choose how much to feed your people]
plantFields

plantFields =
[choose how many acres to sow with seed]
displayYearResults

displayYearResults =
[show what happened as a result of player's choices]
if game not over:
buyLand


I've omitted several additional control branches: If the user inputs nonsensical numbers the original program sometimes prints a huffy message and quits.

There is a basic game state that I pass through all the functions; it contains information that needs to persist from one year of game time to the next. There are additional bits of information that flow between some stages as well. For instance the choice of how many acres to sow with seed is needed in displayYearResults but not needed after that, so I've left it out of the main game state.

Simple as Hamurabi is, I found myself needing to step back and do an even simpler game first. Here's one where the computer picks a number and the player tries to guess it:


-- Computer chooses a number; player guesses what it is
-- Example of basic I/O

import Char
import Random
import IO

minNum = 1
maxNum = 1000

main = do
targetNum <- randomRIO (minNum, maxNum)
putStrLn ("I'm thinking of a number between " ++ (show minNum) ++
" and " ++ (show maxNum) ++ ". Can you guess it?")
guess 1 targetNum

guess :: Int -> Int -> IO ()
guess totalGuesses targetNum = do
putStr ("Guess " ++ (show totalGuesses) ++ ": ")
hFlush stdout
line <- getLine
case (maybeRead line) of
Nothing -> putStrLn ("Give up? The number was " ++ (show targetNum) ++ ".")
Just guessedNum ->
if targetNum == guessedNum then
putStrLn ("You guessed it in " ++ (show totalGuesses) ++ " tries!")
else do
putStrLn hint
guess (totalGuesses + 1) targetNum
where
hint = "My number is " ++ lessOrGreater ++ " than " ++ (show guessedNum) ++ "."
lessOrGreater = if targetNum < guessedNum then "less" else "greater"

maybeRead :: Read a => String -> Maybe a
maybeRead s = case reads s of
[(x, str)] | all isSpace str -> Just x
_ -> Nothing


This should give an idea of the level of verbosity that I'm contending with right now. Hopefully I can improve on this and come up with a clean way to structure the more complex program.

9 comments:

Anonymous said...

Two bits of advice:
(1) Use StateT GameState IO and/or Prompt (the MonadPrompt package on hackage.haskell.org)

(2) Build some combinators, like ths one:
until :: m (Maybe a) -> m a
until act = act >>= maybe (until act) return

which loops until you return a non-nothing value.

Anonymous said...

Here's the "guess a number game" using MonadPrompt.

http://www.mail-archive.com/haskell-cafe@haskell.org/msg34010.html

michael said...

(This is impossible to resist.)

Or ... you can use the embedded BASIC package (http://augustss.blogspot.com/2009_02_01_archive.html ) Good luck with Hamurabi though...

{-# LANGUAGE ExtendedDefaultRules, OverloadedStrings #-}
import BASIC

main = runBASIC $ do
10 GOSUB 1000
20 PRINT "* Welcome to HiLo *"
30 GOSUB 1000

100 LET I := INT(100 * RND(0))
200 PRINT "Guess my number:"
210 INPUT X
220 LET S := SGN(I-X)
230 IF S <> 0 THEN 300

240 FOR X := 1 TO 5
250 PRINT X*X;" You won!"
260 NEXT X
270 STOP

300 IF S <> 1 THEN 400
310 PRINT "Your guess ";X;" is too low."
320 GOTO 200

400 PRINT "Your guess ";X;" is too high."
410 GOTO 200

1000 PRINT "*******************"
1010 RETURN

9999 END

Yair said...

btw: in case you don't know hoogle, I think it might be useful for you.

$ cabal install hoogle
$ hoogle "Bool -> m () -> m ()"
Control.Monad unless :: Monad m => Bool -> m () -> m ()
Control.Monad when :: Monad m => Bool -> m () -> m ()
...
$ hoogle "[Maybe a] -> [a]"
Data.Maybe catMaybes :: [Maybe a] -> [a]
...

Anonymous said...

-- http://playtechs.blogspot.com/2009/06/messing-with-haskell.html

-- Computer chooses a number; player guesses what it is
-- Example of basic I/O

import Random
import Text.Printf
import Control.Monad

minNum = 1
maxNum = 1000

main = do
targetNum <- randomRIO (minNum, maxNum)
printf "I'm thinking of a number between %d and %d. Can you guess it?\n"
minNum maxNum
play targetNum [1..]

play tn (x:xs) = do
m <- guess tn x
unless m $ play tn xs

guess :: Int -> Int -> IO Bool
guess targetnum guessnum = do
printf "Guess %d: " guessnum
num <- liftM read getLine
let r = compare targetnum num
printf "My number is %s yours.\n" (show r)
return $ r == EQ

Michael said...

I forgot to add, regarding the remark

"I wish the Haskell documentation, whether online or provided with the installation, was put together better. I'd like to have ready access to a language reference, not just the auto-generated library documentation; I'd like to have lots of sample code snippets; and I'd like for it to be fully searchable"

Everyone agrees this is true. Keep in mind though that Haskell-land has something better than any documentation -- IT REALLY IS TRUE -- namely the #haskell IRC on freenode. Occasionally you'll have to try a couple of times with your query, but I am forever stunned by the brilliance of the denizens and the bright spirit in which help is given to learners of all levels. It is completely unreal and unparalleled. It very much helps to begin any query with problematic code posted on http://hpaste.org/

sclv said...

A simple but deep point on how to structure haskell programs:

http://www.haskell.org/haskellwiki/Data_structures_not_functions

(The whole "Idioms" category of the wiki is a pretty good read).

James McNeill said...

Thank you all for the help!

The BASIC interpreter is a crazy stunt; I'm going to have to try that out. I've been reading the MonadPrompt stuff to try and understand what it brings to the table.

I have been cruising the "Idioms" section of the Haskell wiki.

I've just about finished my port of Hamurabi using the structure I outlined in my post. After I've finished that I'll try to structure it better. I'm trying to think what the key abstractable things are. Reading a number and validating it with messages is clearly one of them. I've been wondering if I could treat the input as a list of (lazily gotten) lines and if that would simplify anything.

Anonymous said...

Hello everybody! I do not know where to start but hope this site will be useful for me.
In first steps it is very nice if someone supports you, so hope to meet friendly and helpful people here. Let me know if I can help you.
Thanks in advance and good luck! :)