Input

Dear Computer

Chapter 7: Immutability and I/O

Input

Just as programs scatter output, so must they 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
> :t getChar
getChar :: IO Char
> :t getLine
getLine :: IO String

They don't look like functions since there's no ->. But they are functions. They just don't have any parameters. The only way they can return different values on every call is because they rely on the implicit environment passed to them.

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. It's not just these two functions that are tainted. All input and output functions return tainted values.

We can't directly perform any operations on tainted values. Let's try. Suppose we have this function that returns a tainted 1 as an IO Int:

Haskell
getTaintedOne :: IO Int
getTaintedOne = do
  return 1
getTaintedOne :: IO Int
getTaintedOne = do
  return 1

If we load this function in GHCi and try to use the value it returns 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
> 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. We can only do this in the land of impurity—in another do block. The command <- opens the box, extracts the pure value within, and binds a name to it. It's 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)
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 <- command unboxes an IO String from getLine into a String.

Suppose we want the rating as an Int to do math on it. We must unbox it to a String first and then use the read function to convert the String to an Int. Outside of a do block, in a pure context, we would write this conversion expression:

Haskell
read ratingString :: Int
read ratingString :: Int

To do a pure assignment inside of a do block, we don't use <-. Instead we use a let statement that looks just like a declaration we might see in an imperative language:

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

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

← OutputHelper Functions →