Here is a simple Guessing Game I implemented in Haskell
import System.Random
import Text.Read
main :: IO ()
main = do
putStrLn "I am guessing a value between 1 to 100"
guess <- (randomIO :: IO Int)
playWith (guess `mod` 100)
putStrLn "Play again? (Y/N)"
response <- getLine
if response == "Y" then
main
else
return ()
playWith :: Int -> IO ()
playWith n = go 5
where
go 0 = putStrLn "You loose!"
go n = do
putStr "Enter your guess: "
guess <- readUntilValid
if n == guess then
putStrLn "You win!"
else
if n < guess then
putStrLn "Too large!" >> go (n-1)
else
putStrLn "Too small!" >> go (n-1)
readUntilValid :: IO Int
readUntilValid = do
s <- getLine
case (readMaybe s :: Maybe Int) of
Nothing -> putStr "Please enter a valid guess:" >> readUntilValid
Just x -> return x
Is there something I can (or should) do to prevent the usage of goto like recursion? Maybe I could use something from Control.Monad
?
2 Answers 2
Great job on writing readUntilValid
as a separate function instead of inlining it in playWith
. That being said, we can go a little bit further with splitting the functionality of your code.
At the moment, your main
contains five distinct steps:
- greet the user with the rules
- choose a random number
- start the game
- ask whether the user wants to play again
- restart
The random number is part of the game, so it's a little bit strange to generate it in main
. Your function playWith
already suggests a name for a another function that takes care of that step:
play :: IO ()
play = randomRIO (1, 100) >>= playWith
randomRIO
takes a range, so we end up with the correct numbers. Your program choosed numbers between 0-99, by the way.
However, now we can also get the rules in there:
import Control.Monad (when)
import System.Random (randomRIO)
import Text.Read (readMaybe)
play :: IO ()
play = do
putStrLn "I am guessing a value between 1 to 100."
randomRIO (1, 100) >>= playWith
main :: IO ()
main = do
play
response <- prompt "Play again? (Y/N)"
when (response == "Y") main
prompt :: String -> IO String
prompt xs = putStr (xs ++ " ") >> getLine
The prompt
is just a little helper for questions to the user. What did we gain from splitting main
? Well, main
does not have to care for the game at all. We can replace play
by any other game, e.g. playTicTacToe
, and we don't have to change our main
at all. So try to isolate your code a little bit.
Next we have playWith
. There's a bug in there, you use n
twice, once as (correct) answer, once as the number of turns the user has. Therefore, they have to guess 5
, 4
, and so on. The original random number wasn't used at all. Let's fix that first:
playWith :: Int -> IO ()
playWith answer = go 5
where
go 0 = putStrLn "You loose!"
go n = do
putStr "Enter your guess: "
guess <- readUntilValid
if answer == guess then
putStrLn "You win!"
else
if answer < guess then
putStrLn "Too large!" >> go (n-1)
else
putStrLn "Too small!" >> go (n-1)
Note that GHC will warn you about shadowing if you use -Wall
. But we can still do better. If you're going to compare two values and want to act on more than a single outcome, use compare
instead of two checks (here ==
and <
):
playWith :: Int -> IO ()
playWith answer = go 5
where
go 0 = putStrLn "You lost!"
go n = do
putStr "Enter your guess: "
guess <- readUntilValid
case guess `compare` answer of
EQ -> putStrLn "You win!"
LT -> putStrLn "Too small!" >> go (n-1)
GT -> putStrLn "Too large!" >> go (n-1)
And now back to readUntilValid
As I said, it's completely fine, however the additional type signature isn't necessary, as the compiler can infer readMaybe
's type in that context. We end up with:
readUntilValid :: IO Int
readUntilValid = do
s <- getLine
case readMaybe s of
Nothing -> putStr "Please enter a valid guess:" >> readUntilValid
Just x -> return x
Note that you can even write it as
readUntilValid :: Read a => IO a
but that's an aside. We end up with
import Control.Monad (when)
main :: IO ()
main = do
play
response <- prompt "Play again? (Y/N)"
when (response == "Y") main
play :: IO ()
play = do
putStrLn "I am guessing a value between 1 to 100."
randomRIO (1, 100) >>= playWith
playWith :: Int -> IO ()
playWith answer = go 5
where
go 0 = putStrLn "You lost!"
go n = do
guess <- askNumber
case guess `compare` answer of
EQ -> putStrLn "You win!"
LT -> putStrLn "Too small!" >> go (n-1)
GT -> putStrLn "Too large!" >> go (n-1)
prompt :: String -> IO String
prompt xs = putStr (xs ++ " ") >> getLine
askNumber :: IO Int
askNumber = do
response <- prompt "Please enter your guess (1-100): "
case readMaybe response of
Nothing -> putStrLn "Not a valid guess, try again." >> askNumber
Just x -> return x
I've renamed readUntilValid
and also let it ask the number, so that again everything that is concerned with getting input from the user resides in a single function.
All in all, well done. Don't be afraid to split your functions, make sure to enable compiler warnings, and use hlint
(it suggested when
from Control.Monad
, by the way).
Exercises
- at the moment, the user will only replay the game if they enter
Y
. Also accept other inputs, such as "yes", "YES" and "y", and ask again if they don't say "yes" or "no" in a clear way - the number of guesses is currently fixed in
playWith
. Make it variable - the range is currently fixed (1-100). Make it variable
have the user choose a difficulty. The difficulty should determine the original range as well as the number of guesses. Keep the lower difficulty levels fair, e.g.
difficulty | range | guesses --------------+------------+------------------------------- trivial | 1-10 | 10 easy | 1-100 | 10 medium | 1-100 | 7 hard | 1-1000 | 10 -- still fair harder | 1-10000 | 13 -- beginning to get unfair hardest | 1-1000000 | 17 -- missing three guesses lucky | 1-20 | 1
the user doesn't know how many guesses they have. You should probably tell them at some point, especially with difficulty levels
- instead of
()
,playWith
could returndata GameEnd = Won | Lost
, but that's just for style and not necessary. You could use that information for statistics and so on, but that's an overkill
Have fun.
playWith
and go
both call their argument n
, leading to mixups.
mod
maps the integers to [0.99]
, not [1..100]
.
You are looking for Control.Monad.Loops
.
import Control.Monad.Loops (allM, orM, untilJust)
main = void $ allM (== "Y") $ repeat $ do
putStrLn "I am guessing a value between 1 to 100"
value <- randomRIO (1,100) :: IO Int
win <- orM $ replicate 5 $ do
putStr "Enter your guess: "
guess <- untilJust $ do
mguess <- readMaybe <$> getLine :: IO (Maybe Int)
unless (isJust mguess) $ putStr "Please enter a valid guess: "
return mguess
when (guess < value) $ putStrLn "Too large!"
when (guess > value) $ putStrLn "Too small!"
return $ guess == value
putStrLn $ "You " ++ if win then "win!" else "lose!"
putStrLn "Play again? (Y/N)"
getLine
-
5\$\begingroup\$ While the code you present does have the same behaviour, it's an entirely different ballpark of complex. I don't like how you've inlined both extracted functions, while rewriting them significantly without any comment. I think it would help OP a lot if you could be significantly more explicit in your rewriting process \$\endgroup\$Vogel612– Vogel6122017年11月08日 00:08:31 +00:00Commented Nov 8, 2017 at 0:08
-
\$\begingroup\$ If extracting the inner do blocks would help, the reader can simply ignore all code that starts successively further from the left for the same effect. \$\endgroup\$Gurkenglas– Gurkenglas2017年11月08日 01:59:45 +00:00Commented Nov 8, 2017 at 1:59