Skip to content
AgileLogics
6 min read

IO and do-notation

Prerequisites: Monads

A value of type IO a is a description of an action that, when the runtime executes it, may interact with the world and produces an a. Building such a description does nothing; only actions reachable from main ever run.

main :: IO ()
main = do
  let greet = putStrLn "hello"
  putStrLn "before"
  greet
  greet
  let actions = [putStrLn "one", putStrLn "two"]
  sequence_ (reverse actions)
before
hello
hello
two
one

greet is a value — an action — that ran twice because it was used twice in main. Actions can be stored in lists and combined like any other value.

Rules of do blocks

import Data.Char (toUpper)

shout :: String -> IO String
shout s = do
  let loud = map toUpper s
  putStrLn ("Shouting: " ++ loud)
  return (loud ++ "!")

main :: IO ()
main = do
  result <- shout "hey"
  print result
  n <- return 5
  if n > 3
    then putStrLn "big"
    else putStrLn "small"
  mapM_ print [length result, n]
Shouting: HEY
"HEY!"
big
4
5
  • x <- action runs an action and binds its result.
  • let x = expr binds a pure value.
  • A line on its own runs an action and ignores its result.
  • The last line is the block's result, so its type decides the type of the whole block.
  • Both branches of if inside do must be actions of the same type.

Pure core, IO shell

-- Try it: printf '3 5 7\n10 20\n' | runghc main.hs
stats :: [Int] -> String
stats xs = "sum=" ++ show (sum xs) ++ " max=" ++ show (maximum xs)

processLine :: String -> String
processLine = stats . map read . words

main :: IO ()
main = do
  contents <- getContents
  mapM_ (putStrLn . processLine) (lines contents)
sum=15 max=7
sum=30 max=20

All the logic is in pure functions (stats, processLine) that you can test in GHCi. main just reads, calls them, and prints. This structure is the most important design habit in Haskell.

Loops in IO

import Control.Monad (forM, forM_)
import Data.IORef

main :: IO ()
main = do
  squares <- forM [1 .. 5] $ \i -> do
    return (i * i)
  print squares
  counter <- newIORef (0 :: Int)
  forM_ [1 .. 10] $ \i -> modifyIORef' counter (+ i)
  readIORef counter >>= print
[1,4,9,16,25]
55

IORef is a mutable variable that lives in IO. You rarely need one — folds and recursion usually express the same thing — but it's there when an imperative style is genuinely clearer.

Practice

Write a program that reads lines until it sees quit, then prints how many lines it read (use recursion with a counter).

Show solution
-- Try it: printf 'hello\nworld\nquit\n' | runghc main.hs
loop :: Int -> IO Int
loop n = do
  line <- getLine
  if line == "quit"
    then return n
    else loop (n + 1)

main :: IO ()
main = do
  count <- loop 0
  putStrLn ("Read " ++ show count ++ " lines")
Read 2 lines