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.
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).
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
randomRIO
as Daniel mentioned, since we're already in the IO monad.hSetBuffering
or hFlush
using the command prompt on Windows to get correct output. YMMV, however.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With