Guessing Game

Dear Computer

Chapter 7: Immutability and I/O

Guessing Game

Let's examine how we might implement a quaint guess-a-number game in Haskell. The computer picks a number at random in [0, 100]. Then the user makes a guess, and the computer says whether the guess is too low, too high, or just right. If the guess isn't right, the user keeps trying, and the computer keeps giving feedback.

The first task is to generate a random number. There's no random number generator built in to the Haskell standard library. We could write our own with linear congruence, but designing one that has a uniform distribution is challenging. A better option is to use the System.Random module, which can be installed with this single shell command:

Shell
cabal install --lib random

The cabal package manager was likely installed with the rest of the Haskell platform.

The randomRIO function from this module generates a random number in a range and effectively has this type signature:

Haskell
randomRIO :: (a, a) -> IO a
randomRIO (lo, hi) = ...

It accepts a tuple of a lower and upper bound. It returns a tainted random number within the given range.

Because randomRIO is an impure function, we call it from a do block and use the <- operator to unpack the random number from its IO box. Additionally, we indicate what type of number we want with a type annotation. This main generates and prints a random integer in the range [0, 100]:

Haskell
import System.Random

main = do
  target <- randomRIO (0, 100) :: IO Int
  print target

The input-output interaction needs to happen repeatedly, so we factor it out into a recursive helper function that accepts the target number:

Haskell
playRound :: Int -> IO ()
playRound target = do
  -- prompt the user
  -- get guess
  -- if guess is target
  --   congrats
  -- else
  --   give feedback
  --   recurse

The comments are translated more or less directly into Haskell. The putStr function prompts. The getInt function we wrote earlier gets the user input. The feedback requires a conditional expression of some form. This implementation uses two if-else expressions:

Haskell
import System.Random
import Text.Read

getInt :: IO Int
getInt = do
  intString <- getLine
  let intMaybe = readMaybe intString :: Maybe Int
  case intMaybe of
    Just int -> return int
    Nothing -> do
      putStr "That wasn't an integer. Try again: "
      getInt

playRound :: Int -> IO ()
playRound target = do
  putStr "Guess a number: "
  guess <- getInt
  if guess == target then
    putStrLn ("Correct! The number is " ++ show target ++ ".")
  else if guess < target then do
    putStrLn (show guess ++ " is too low.\n")
    playRound target
  else do
    putStrLn (show guess ++ " is too high.\n")
    playRound target

main = do
  target <- randomRIO (0, 100) :: IO Int
  playRound target

Whenever a then or else branch has multiple statements, they are sequenced together in a do block.

← FilesAcrostic →