Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to keep track of number of guesses in a simple guessing game (Haskell)

I've been attempting to learn Haskell independently in the last few weeks. Currently, I'm trying to implement a goofy little guessing game where the computer chooses a random number and the user tries to guess it. If the user is wrong, the program tells the user either that the answer is higher or lower and allows the user to guess until they guess correctly. I've got it working, but I would like to add the ability to keep track of the number of guesses that the user makes every game and report that number to the user once they guess correctly.

Coming from an imperative background, the natural thing to do would be to have a counter that is incremented every time the user makes a guess, but you can't really do that in Haskell (at least it seems like the statelessness and immutability of everything would prevent that).

I toyed with the idea of making the getGuess and giveHints functions take an extra parameter that represents the number of guesses so far (let's call it numGuesses), and, on every call to those methods, pass (numGuesses+1). But I couldn't get that to work (not to mention I don't even know if that would work).

My code is below. Any suggestions would be really appreciated. I'm mostly looking for ideas, but feel free to post actual code as well. Also, feel free to let me know if my code sucks and how I could improve it if you notice anything heinous (I've only been programming functionally for a couple weeks!)

    import System.Random
    import System.IO
    import Control.Monad

    main = do
        gen <- getStdGen
        let (ans,_) = randomR (1,100) gen :: (Int,StdGen)
        putStrLn $ "I'm thinking of a number between 1 and 100..."
        getGuess ans
        putStrLn "You guessed it in __ guesses!"
        putStr "Play again? "
        hFlush stdout
        desire <- getLine
        when ((desire !! 0) `elem` ['y','Y']) $ do
            putStrLn ""
            newStdGen
            main

    getGuess ans = do   
        putStr "Your guess? "
        hFlush stdout
        guessStr <- getLine
        giveHints ans (read guessStr)

    giveHints ans guess = do
        when (ans /= guess) $ do
           if ans > guess 
               then putStrLn "It's higher." 
               else putStrLn "It's lower."
           getGuess ans

Note: I am using hFlush stdout because I'm using line buffering and, without it, the order of some of the interactions are not what one would expect.

like image 202
mkfarrow Avatar asked Aug 08 '12 00:08

mkfarrow


2 Answers

Adding an extra parameter for the number of guesses is exactly how you do this sort of thing functionally.

The basic functional mode of thinking is that if you have a function that needs to behave differently depending on different values of "something", then that something is a parameter to the function. This is a simple consequence of purity; a function must always return the same thing for the same inputs.

When you get to more advanced techniques there are various ways of "hiding" the extra parameters to free you up from having to write/pass them explicitly; this is basically exactly what the State monad does, and one way of thinking about the IO monad is that it's doing something similar. But while you're new to functional programming it's probably more helpful to get used to this mode of thinking; you communicate information to a function you're calling through its arguments, and receive information back through its arguments. You can't resort to the imperative trick of just leaving information in some external place (e.g. the value of a counter) where you know the function you call will look for it (or even modify it).

like image 20
Ben Avatar answered Oct 19 '22 23:10

Ben


You can in fact implement the counting method you were thinking about, but you still have to pass the state around explicitly. But in this case, it isn't much of a hassle at all. In fact, this is a pattern that one sees pretty often for helper functions, where actually using the State monad would be overkill.

The pattern I'm referring to often looks like this:

doStuff xs' = go xs' 0
  where
    go (x:xs) n = .. etc ..

Here's the code.

import System.Random       (randomRIO)
import Control.Applicative ((<$>))
import Control.Monad       (when)
import Text.Printf         (printf)  

playGame :: Int -> Int -> IO ()
playGame answer curGuesses = do
    putStrLn "What is your guess?"
    putStr   ">"
    guess <- getGuessFromUser
    when (guess /= answer) $ do
        giveHints answer guess
        playGame answer (curGuesses + 1)
    when (guess == answer) $ do
        putStrLn "You guessed it!"
        printf   "You guessed %d times!\n" (curGuesses + 1)

giveHints :: Int -> Int -> IO ()
giveHints answer guess 
    | answer > guess = putStrLn "It's higher!"
    | otherwise      = putStrLn "It's lower!"

getGuessFromUser :: IO Int
getGuessFromUser = do
    read <$> getLine

main :: IO ()
main = do
    answer <- randomRIO (1, 100)
    putStrLn "I'm thinking of a number between 1 and 100."
    playGame answer 0

Notes

  • <$> is fmap
  • I used randomRIO as Daniel mentioned, since we're already in the IO monad.
  • I did not have to use hSetBuffering or hFlush using the command prompt on Windows to get correct output. YMMV, however.
like image 135
identity Avatar answered Oct 20 '22 01:10

identity