Monday, November 13, 2006

Haskell Golf Scores

I am tutoring someone in Java. We did an example program of summing up the golf scores of a number of people. I was interested in showing him some MVC design principles, as it will be useful for his end-of-year project.

I decided to improve my understanding of Haskell today, by reimplementing it in Haskell. It lacks MVCishness, but it's still nice. I also couldn't find a function to convert from strings to integers, so I implemented one quickly using a number->int function. It is not perfect, since it does conversions to hex digits, but it is a start.

Can I improve on it? Please do comment. ;)

module GolfScore where

import Data.Char

toNumber str =
let slen = length str
in
toNum_ str ((length str) - 1) 0

toNum_ :: [Char] -> Int -> Int -> Int
toNum_ str (-1) val = val

toNum_ str count val =
let currVal = (str !! count)
in toNum_ str (count - 1)
(val + ((digitToInt currVal) * (10 ^ (((length str) - 1) - count))))

----------
-- main
----------
run count holes = run_ count holes [] []

run_ 0 holes lstName lstRes = showResults (reverse lstName) (reverse lstRes)
run_ count holes lstName lstRes =
do putStr "Enter name: "
n <- getLine
scores <- getAndAddScores holes []

run_ (count -1) holes (n : lstName)
((foldr ((+) . toNumber) 0 scores) : lstRes)


showResults [] _ = return ()
showResults (x:xs) (y : ys) =
do
putStr x
putStr " got a score of: "
print y
showResults xs ys


getAndAddScores 0 scores = do return scores
getAndAddScores holes scores =
do
putStr "Enter score: "
score <- getLine
getAndAddScores (holes - 1) (score : scores)

7 Comments:

Blogger Bryan said...

The 'read' function is useful for translating a String to an Integer (or any other data type in the Read class).

> read "36" :: Integer
36

> 7 + (read "19")
26

> read "[1,2,3]" :: [Integer]
[1,2,3]

7:01 PM  
Anonymous Brandon said...

The easiest step to improve your code is using library functions for recursion patterns.

showResults = zipWithM showResult
where showResult x y = putStrLn (x ++ " got a score of " ++ show y)

getScores count =
replicateM count (putStr "Enter score:" >> getLine)

The scoring in run can be simplified a little bit.

foldr ((+) . read) 0 scores ==
foldr (+) 0 (map read scores) ==
sum (map read scores)

run can be done with replicateM, if it accumulates results as a list of pairs rather than a pair of lists.

run count holes = do
results <- replicateM count (do
putStr "Enter name:"
name <-getLine
scores <- getScores holes
return (name, scores))
showResults (unzip results)

For good code, I would split this (trivially) into a game reading and a game printing function.

For maximum brevity, inline the helpers and create the output IO actions while reading in the game.

run count holes =
sequence_ =<< replicateM count (do
name <- putStr "Enter name:" >> getLine
scores <- replicateM holes (putStr "Enter Score:" >> getLine)
return $ putStrLn $ name ++ " got a score of "++show (sum (map read scores)))

2:45 AM  
Blogger fusiongyro said...

I hope you're not tired of receiving other people's implementations on here... I am learning myself and thought a reimplementation of your program here would be fun. And it was! So I hope my version elucidates some things, with a little commentary.

It took a long time for me to get comfortable enough with monads that I could do IO the way I do in this program. I laud your implementation and I hope you'll keep on keepin' on. Haskell is hard to learn and the type system often doesn't help. I started including annotations because everyone else does and it actually seems to help find bugs and improve the design.

> module GolfScore where
> import Data.List -- for the sort function

One thing a friend told me is that you are going to want functions that take a simple type and return stuff inside a monad, rather than monkeying around with monad -> monad typed functions. Actually in general do it as purely as possible; if you have to return in a monad that's fine but if you can return a pure value instead, do that; there are ways of lifting that function into the monad when you have to. Because the monad operations are all intended to help you glue functions with that kind of type together. Which is what I've got in this code:

> getName :: IO String
> getName = putStr "Enter name: " >> getLine
>
> getScore :: IO Integer
> getScore = putStr "Enter score: " >> getLine >>= return . read

Then I combined these primitives to make one which handles getting a person's name and score. I decided it would be more convenient to carry them around in a 2-tuple rather than two lists, because it makes the relationship concrete.

> getNameAndScore :: Int -> IO (String, Integer)
> getNameAndScore holes = do
> name <- getName
> scores <- sequence $ replicate holes getScore
> return (name, sum scores)

Sequence and replicate form the basis of my iteration in this program. It lets me avoid any explicit looping, because we're really just doing the same things over again and collecting the result. I recommend spending time reading the prelude, it helped me quite a bit to see what's there and built-in already. Anyhow, sequence and sequence_ are handy because they let you take a list of monadic actions and convert them into a list of results. mapM is similar, essentially applying a monad action to a list and collecting the result.

Here are my output functions:

> showResult :: String -> Integer -> IO ()
> showResult name score = putStrLn $ name ++ " got a score of " ++ (show score)
>
> showResults :: [(String, Integer)] -> IO ()
> showResults = mapM_ (uncurry showResult)

So showResults is clearly intended to take the output of getNameAndScore. showResult probably could have been coded to take a 2-tuple but I like using uncurry and I don't like writing functions that take tuples unless I really have to. Pet peeve I guess. And there's the aforementioned mapM. The underscore variants tend to throw away their results, which is fine here, because showResult produces IO (), and mapM would give me back a list of [IO (), IO ()...]. Not something I care to keep around.

And here is your essential function. I did add one thing, it sorts by score, showing the winner at the top. :)

> run :: Int -> Int -> IO ()
> run count holes = do
> namesAndScores <- sequence $ replicate count (getNameAndScore holes)
> let sorted = sortBy (\l r -> compare (snd l) (snd r)) namesAndScores
> showResults sorted

The sequence $ replicate construct there is the same as the one above, creating a list of monad actions and then evaluating them. I had a tendency when I was starting to try and write the second line as "sorted <- sortBy ..." but that of course doesn't work because it is a pure function result. The let solves this problem. sortBy takes a function and a list; my function just pushes the compare into the second item of the tuple and sorts based on that, which happens to be the score. Then I show the results.

2:36 PM  
Blogger Howard said...

I'm a beginner trying to just getting stuff running - so I created golf.hs, pasted your code into it, and tried to run it in both ghci & winghci. But I'm guessing how to run it.

Here's what Winghci tells me:
:1:0: Not in scope: `main'
*GolfScore> Main


:1:0: Not in scope: data constructor `Main'
*GolfScore> getAndAddScores


:1:0:
No instance for (Show (t -> [String] -> IO [String]))
arising from a use of `print' at :1:0-14
Possible fix:
add an instance declaration for
(Show (t -> [String] -> IO [String]))
In a stmt of a 'do' expression: print it
*GolfScore>

2:00 PM  
Blogger Howard said...

I'm a beginner trying to just getting stuff running - so I created golf.hs, pasted your code into it, and tried to run it in both ghci & winghci. But I'm guessing how to run it.

Here's what Winghci tells me:
:1:0: Not in scope: `main'
*GolfScore> Main


:1:0: Not in scope: data constructor `Main'
*GolfScore> getAndAddScores


:1:0:
No instance for (Show (t -> [String] -> IO [String]))
arising from a use of `print' at :1:0-14
Possible fix:
add an instance declaration for
(Show (t -> [String] -> IO [String]))
In a stmt of a 'do' expression: print it
*GolfScore>

2:01 PM  
Anonymous Anonymous said...

My point of view partially coincided with yours. Thank you for trying. antidepressants Read a useful article about tramadol tramadol

4:39 AM  
Anonymous Anonymous said...

Helo ! Forex - Работа на дому чашкой чая получать удовлетворение от работы получатьприбыль, просто зарегистрируйтесь forex [url=http://foxfox.ifxworld.com/]forex[/url]

10:06 AM  

Post a Comment

<< Home