Input

Dear Computer

Chapter 7: Immutability and I/O

Input

Programs also need a way to gather input. Haskell provides the getLine and getChar functions to read text from standard input. Consider their type signatures:

GHCI
> :t getChar
getChar :: IO Char
> :t getLine
getLine :: IO String

When we call them, we don't get back a plain Char or a plain String. We get back IO Char and IO String. To retrieve the input from the user, these functions had to pass into the land of impurity—where new state is made. Any value we bring back from this land is packaged up in a box marked IO, which means the value is “tainted” by state. We can't perform any operations on values inside IO boxes.

Suppose we have this function that returns a tainted 1 as an IO Int:

Haskell
getTaintedOne :: IO Int
getTaintedOne = do
  return 1

If we load this function in ghci and try to use it in an expression, the code fails to typecheck:

GHCI
> getTaintedOne + 2
<interactive>:3:15: error:
    • No instance for (Num (IO Int)) arising from a use of ‘+’
    • In the expression: getTaintedOne + 2
      In an equation for ‘it’: it = getTaintedOne + 2

To work with a value tainted by IO, we must first open up the box, which we can only do in the land of impurity—in another do block. The binding operator <- opens the box and extracts the pure value within. This operator is a bit like an assignment operator, but it turns an IO Char into a normal Char or an IO Int into a normal Int. In this program, it is used to collect a string from the user:

Haskell
main = do
  putStr "On a log scale from 1 to 10, how are you? "
  ratingString <- getLine
  putStrLn ("I'm sorry you are at " ++ ratingString)

Here the <- operator unpacks an IO String into a String. If we want the rating as an Int, we must unpack it to a String first and then use the read function to convert the String to an Int. Outside a do block, in a pure context, we would write this conversion expression:

Haskell
read ratingString :: Int

To assign the value of this expression to a variable, we don't use <-, which is strictly for opening boxes tainted by state and not a general assignment operator. To do a normal assignment, we could use a where clause. However, a let statement looks more like the declarations we've seen in imperative languages:

Haskell
main = do
  putStr "On a log scale from 1 to 10, how are you? "
  ratingString <- getLine
  let rating = read ratingString :: Int
  putStrLn ("I'm at " ++ show (1 + rating))   

Haskell do blocks contain a mix of unboxing statements that use the <- operator and the pure assignment statements that use the = operator. Be careful! It's easy to mix them up.

← OutputHelper Functions →