tobold.org

correct • elegant • free

△ How to do IO in Haskell △

◅ More output functions

return is not control-flow syntax ▻

Input, binding

So far, our IO examples have included plenty of O, but no I at all! Let's start very simply, with hello.hs.

main = do
  putStr "What is your name? "
  n <- getLine
  putStrLn ("Hello, " ++ n ++ "!")

This example introduces the function getLine :: IO String, which reads a line from standard input. Just for now, we will say that functions like getLine are "IO computations" and return a value which is "IO encumbered".

This example also demonstrates some new syntax, and the second sort of thing we can put in a do expression. The line n <- getLine binds a name n to the value returned by the IO computation getLine, removing the IO encumbering. So in this example, the type of n is n :: String, which makes it a suitable candidate for the string concatenations in the next line.

Unsurprisingly, names bound by <- in a do expression are in scope till the end of the do expression.

The only way to retrieve a value from an IO computation, and remove the IO encumbrance, is to bind a name to the value with <- in a do expression. It's easy to think that getLine is returning a plain String value, and try to write code like badhello.hs.

main = do
  putStr "What is your name? "
  putStrLn ("Hello, " ++ getLine ++ "!") -- illegal

This won't work, and the compiler will tell you so in no uncertain terms. (If you know much about lazy evaluation, you'll see that even if it didn't contain a type error, this example probably wouldn't do what we intend: it would start writing Hello before calling getLine. Remember that do helps us to ensure that IO actions occur in a useful sequence.)

Just as we can define new IO actions, we can define our own IO computations, as we see in prompt.hs.

main = do
  n <- prompt "What is your name? "
  putStrLn ("Hello " ++ n ++ "!")

prompt p = do
  putStr p
  getLine

This behaves just like the first input example, of course, but we have defined a new function prompt which has type prompt :: String -> IO String.

In this case, the value returned by getLine is simply propagated by prompt. Life gets more interesting when we construct new values, as we see in countlines0.hs.

main = do
  l <- countLines "/etc/passwd"
  putStrLn (show l)

countLines f = do
  x <- readFile f
  return (length (lines x))

This example introduces the function readFile :: FilePath -> IO String, which returns the entire contents of the file as a single string. We have used readFile to define a new function countLines with type countLines :: FilePath -> IO Int (where FilePath is a type synonym for String).

The interesting part is the function return, which has the polymorphic type return :: a -> IO a [1]. In other words, return takes an argument of any type at all, and "IO encumbers" it. In this case, a is Int (the return type of length), so return produces a value of type IO Int, and this is also the type of the entire do expression and hence the return type of countLines itself.

[1]Actually, return is a class method for any instance of Monad, not just IO, and its real type is return :: (Monad a) => b -> a b. But we're trying to avoid Monad theory.

So far we've seen IO computations that return values of types IO String and IO Int, but there are (literally) infinite other possibilities. Here is echo0.hs, a Haskell implementation of the Unix echo command, which simply writes its command-line arguments to standard output, separated by spaces.

import Data.List (intersperse)
import System.Environment (getArgs)
    
main = do
  args <- getArgs
  putStrLn (concat (intersperse " " args))

This example introduces the function getArgs (from the System module), which has type getArgs :: IO [String]. It returns a list of the command-line arguments (not including the command name).

Here's echo1.hs, a slightly different, but exactly equivalent, way of writing the previous example.

import Data.List (intersperse)
import System.Environment (getArgs)
    
main = do
  args <- getArgs
  let r = concat (intersperse " " args)
  putStrLn r

Finally we meet the third sort of thing that we can put in a do expression: a let expression. Note that there is no in keyword; the names bound by let are in scope for the remainder of the do expression.

Apart from that, let inside do behaves exactly like a normal let. In particular, you can use layout to bind several names at once. The definitions in such a "multi-let" can even be mutually recursive, as in multilet.hs, an admittedly contrived example.

showGt a b = do
  let
      g1 x y = if x >= y then show x else g2 x y
      g2 x y = g1 y x
  putStrLn (g1 a b)

△ How to do IO in Haskell △

◅ More output functions

return is not control-flow syntax ▻