Before you begin
Why this matters
Most surprises in Haskell IO code come from mixing up an action and its result: an effect that happens twice, an effect that never happens, or a return that was meant to stop. If you can tell when an action runs, you can predict what a do block prints, and how often.
What this helps you reason about
- Tell from a type, and from <- or let, whether code names an action or its result.
- Predict how many times an action runs, also when it is stored in a list or passed to a function.
- Spot a return or a when that was meant to stop a do block, and write the choice that does.
In Python, Java, or JavaScript, a call such as input() or print(…) does its work at the moment its line runs. In Haskell, getLine and putStrLn "hi" do nothing on their own: each is an IO action, a value that describes an effect. Side effects and pure functions showed that only IO actions can have effects. This lesson separates making an action from running it, and an action from the result it gives.
Already know this? The short version
- An IO a is an action that gives an a each time it runs; name <- action runs it and names the result, and let name = action names the action and runs nothing.
- Actions are values: they can be stored in lists, passed to functions, and returned, and they run only when main’s do block reaches them; sequence_ runs a list of actions in order, and each run repeats the effect.
- return x does nothing and gives x, without ending the do block; when and unless from Control.Monad choose whether one action runs, so to skip the rest, put the choice on the last line.
- Keep a pure core that decides what to do and a thin IO shell that runs it. The section “What goes wrong with IO actions in real programs?” covers an action run twice where one result was meant, work done halfway, an error that return delays, and unsafePerformIO.
Words in this lesson (8)
- IO action
- A value that describes something to do in the outside world, such as printing a line. putStrLn "race" is an IO action; it is carried out when main reaches it.
- do block
- A way to write a sequence of actions, one per line. IO actions in a do block run in the order they are written. do works for other kinds of actions too, such as Maybe computations.
- result of an action
- The value an IO action gives when it runs, such as the line that getLine reads. In a do block, name <- action names the result. The action and its result have different types, such as IO String and String.
- sequence_
- A function that takes a list of actions and gives one action that runs them in list order and keeps none of their results.
- return
- A function that wraps a value in an action: return x does nothing when it runs and gives x as its result. It does not end a do block.
- unit value ()
- The value written (), and also the name of its type. An action with nothing useful to give, such as putStrLn "hi", gives ().
- when and unless
- Functions from Control.Monad that choose whether one action runs. when test action runs the action if the test is True, and unless runs it if the test is False; otherwise each does nothing.
- pure core
- The part of a program made of pure functions that compute what to do. A small IO part, the shell, reads the inputs and runs the actions the core describes.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Action and result
What is the difference between an IO action and its result?
Expressions and statements showed that an IO action is a value, and that name <- action runs the action and names its result. This section looks at the two halves apart: the action, and the result of the action.
The type says which is which. getLine :: IO String is an action. When it runs, its result is a String: the line that was read. An IO String is not a String. It is a description of how to get one. The same action can run many times and give a different result each time, because the result comes from the outside world at the moment the action runs.
So <- and let name different things. name <- getLine runs the action and names its result, a String. let again = getLine names the action itself, and runs nothing. Using again as text is a type error: Main.hs:4:28: error: Couldn’t match expected type: [Char] with actual type: IO String. [Char] is another way to write String: GHC expected text and found an action that would give text.
main :: IO ()
main = do
name <- getLine -- runs getLine; name is a String
let again = getLine -- names the action; again is an IO String
putStrLn ("welcome, " ++ name)
-- "welcome, " ++ again would be a type error An IO a is an action that gives an a when it runs; <- runs it and names the result, and let names the action.
Coming from Python: name = input() runs input at once and stores the text. let name = getLine runs nothing: name is the action, and name <- getLine is the line that reads.
Precisely: what an action is
The Haskell 2010 Report makes IO an abstract type: a program cannot look inside an action, only combine it with other actions and let main run it. GHC represents an IO a as a function that takes a token for the state of the outside world and gives a new token together with an a. Passing the token from one action to the next fixes their order. Code never sees the token.
Experiment
Run one action twice and compare the results
nextVisitor is one value of type IO Int. Each <- runs it, so the effect happens twice, and the two runs give two results. let second = first names the result of the first run, a plain Int, and runs nothing.
Why it matters To use one result in two places, run the action once with <- and use the name of its result. Naming the action again runs the effect again.
nextVisitor adds 1 to the number in the cell gate and gives the new number: modifyIORef' gate (+ 1) adds 1 to what gate holds, and readIORef gate gives it. Predict what print shows. Then replace line 10 with let second = first and predict again.
import Data.IORef
main :: IO ()
main = do
gate <- newIORef (40 :: Int)
let nextVisitor = do
modifyIORef' gate (+ 1)
readIORef gate
first <- nextVisitor
second <- nextVisitor
print (first, second)
What to look for
Each stop shows one place, such as Main.main.nextVisitor, not a list of frames. At line 9, nextVisitor shows _ (unevaluated): so far it is only a name for an action. While first <- nextVisitor runs, the stepper stops on lines 6, 7, and 8. gate shows GHC.IORef.IORef (GHC.STRef.STRef (GHC.Prim.MutVar# 40)); the number at the end is what the cell holds, and it reads 41 at the stop on line 8. The second run makes no stops inside nextVisitor: GHCi stops where an expression is evaluated, and the action was built during its first run. At line 11, first shows 41 and second shows 42, and the run prints (41,42). After the edit, the action runs once. At line 11, second shows _ (unevaluated); the next stop is Main.main.second on line 10, after (41, has printed, and the run prints (41,41).
Expected output
(41,42)02 / Actions as values
What can code do with an action before it runs?
An action is an ordinary value, so code can do with it what it does with any value. It can give the action a name, put it in a list, pass it to a function, and return it from a function. None of this runs the action.
ring :: Int -> IO () is a function that gives an action. ring 3 builds the action that prints bell 3; applying ring prints nothing. map ring [1, 2, 3] is a list of three actions, of type [IO ()]. Haskell recursion and collection transformations showed that such a list runs nothing as a line of a do block.
An action runs only when it becomes part of main: as a line of main’s do block, or inside an action that main runs. sequence_ takes a list of actions and gives one action that runs them in list order and keeps none of their results. So code can build actions in one place and choose their order, and how often they run, in another. Each run is a new run: an action that is in the list twice runs twice.
ring :: Int -> IO ()
ring bell = putStrLn ("bell " ++ show bell)
main :: IO ()
main = do
let bells = map ring [1, 2, 3] -- prints nothing
sequence_ (reverse bells) -- bell 3, bell 2, bell 1 Building, naming, or storing an action runs nothing; the action runs each time main’s do block reaches it.
Coming from JavaScript: [1, 2, 3].map(ring) calls ring three times, and each call prints at once. map ring [1, 2, 3] in Haskell only builds three actions, and nothing prints until they run.
Precisely: sequence_ and mapM_
mapM_ f xs is the same action as sequence_ (map f xs). Both come from the Prelude and work for any container that can be walked in order, such as a Maybe, not only for lists. sequence, without the underscore, keeps the results and gives them as a list.
Experiment
Build three actions and choose when they run
map ring [1, 2, 3] applies ring to each number and gives three actions; nothing runs. length counts the actions without running them. sequence_ runs the actions of a list in list order, and the edited list holds each action twice, so each one runs twice.
Why it matters The line where an action is written decides nothing. An action runs where main’s do block reaches it, as many times as it is reached.
bells is a list of three actions, and evening is the same list reversed. Predict what prints, and whether line 8 rings any bell. Then change line 9 to sequence_ (evening ++ evening) and predict again.
ring :: Int -> IO ()
ring bell = putStrLn ("bell " ++ show bell)
main :: IO ()
main = do
let bells = map ring [1, 2, 3]
evening = reverse bells
putStrLn ("bells made: " ++ show (length bells))
sequence_ evening
What to look for
At line 8, bells shows _ (unevaluated). The output shows bells made: before the 3, because show computes the number only when putStrLn needs it. The run prints bells made: 3 with no stop in ring. At the stop on line 7, bells shows [_ (unevaluated),_ (unevaluated),_ (unevaluated)]: length counted three cells and built none of the actions. Then sequence_ runs the actions, last first. The stepper stops in ring with bell = 3 while that action is built, and bell 3 appears; then the same for 2 and 1. After the edit, the run prints bell 3, bell 2, bell 1, and then the same three lines again. The stepper stops in ring only for the first three lines. The second three print with no stops, because those actions are already built and only run again.
Expected output
bells made: 3
bell 3
bell 2
bell 103 / return and when
Does return leave a do block?
Function calls showed that return does not end anything in Haskell. return x is an action that does nothing when it runs and gives x as its result. As a line of a do block, return () runs, gives the unit value (), and the next line runs.
when and unless, from Control.Monad, run an action only when a test holds. when test action runs the action if the test is True; otherwise it does nothing, like return (). unless test action runs the action if the test is False. Both choose whether one action runs. Neither one stops the lines after it.
So a guard in the style of Python or Java, when full (return ()), changes nothing. When the test is True it runs return (), which does nothing, and the next lines still run. To skip the rest, make the choice the last line of the block: an if whose then and else sides are each an action.
import Control.Monad (when)
enter :: Int -> IO ()
enter inside = do
when (inside >= 30) (return ()) -- does not leave enter
putStrLn "a visitor enters" -- runs for 30 too
enterChecked :: Int -> IO ()
enterChecked inside =
if inside >= 30
then putStrLn "garden full"
else putStrLn "a visitor enters" return x gives x and ends nothing, and when and unless choose whether one action runs. To skip the rest, put the choice on the last line.
Coming from Java: In Java, return; leaves the method. In a Haskell do block, return () is one more action, and the lines after it still run.
Precisely: why return cannot stop anything
A do block joins each line to the lines after it: do { a; rest } means a >> do { rest }, where >> runs a and then the rest. return () >> rest is the same action as rest, which is why return cannot stop anything. The Haskell 2010 Report gives this translation of do in its section on do expressions.
Experiment
Try to stop a do block with when
when runs its action when the test is True, and then the do block goes on to the next line, whatever the test gave. Nothing in when can stop the lines after it. unless puts the second message under a test of its own.
Why it matters A test in a do block guards only the action it is given. Put every action that must be skipped under the test, or choose with if on the last line.
A visitor who arrives when 30 are inside should see garden full and not enter. Predict the output lines. Then change line 6 to unless (inside >= 30) (putStrLn ("visitor " ++ show (inside + 1) ++ " enters")) and predict again; unless is already imported.
import Control.Monad (unless, when)
enter :: Int -> IO ()
enter inside = do
when (inside >= 30) (putStrLn "garden full")
putStrLn ("visitor " ++ show (inside + 1) ++ " enters")
main :: IO ()
main = do
enter 12
enter 30
What to look for
enter 12 passes line 5 without printing and prints visitor 13 enters. For enter 30, the stepper stops on line 5, garden full prints, and the next stop is on line 6, which prints visitor 31 enters. The run prints visitor 13 enters, garden full, and visitor 31 enters. After the edit, enter 30 still stops on line 6, but nothing prints there, and the run ends after garden full.
Expected output
visitor 13 enters
garden full
visitor 31 enters04 / Pure core
Where should the effects go?
Side effects and pure functions recommended a pure core: main reads the inputs, pure functions compute, and main prints. With actions as values, the IO part can shrink to a few lines.
Let the pure core decide what to do, and give the decision as data. notices below is pure: it gives the lines to show, and prints nothing. main only runs one action for each line. A test can compare notices 30 with the list it expects, without running anything.
A function can also take the actions it needs as parameters. A function with a parameter of type String -> IO () can be given putStrLn in main, and a harmless action in a test, such as one that adds each line to an IORef. The caller then chooses the effect. References, pointers, and handles shows IORefs in more detail.
notices :: Int -> [String]
notices minutes
| minutes <= 0 = ["the garden is closed"]
| otherwise =
("closing in " ++ show minutes ++ " minutes")
: notices (minutes - 15)
main :: IO ()
main = mapM_ putStrLn (notices 30)
-- closing in 30 minutes
-- closing in 15 minutes
-- the garden is closed Compute what to do in pure code, and keep the IO part to a few lines that run the result.
Precisely: demand still applies
mapM_ asks notices for one line at a time, so in the stepper the stops in notices come between the printed lines. Pure code computes nothing until an action needs its value. The order of the effects is still the order of the list.
05 / In production
What goes wrong with IO actions in real programs?
Four mistakes with IO actions are common in real Haskell code.
An action run twice where one result was meant. getCurrentTime, from the time package, reads the clock. Code that runs it once for the log and once for the screen gets two different times. Passing the action to a helper has the same effect: each <- inside the helper runs it again. Run the action once with <-, and pass the result.
Work done halfway. A function that checks each item and writes it at once has already written the first items when a later check fails. When the work must happen for all items or for none, check all of them in pure code first, and start writing only when every check has passed.
An error that appears far from its cause. return x does not compute x. share <- return (div 120 0) succeeds, and the program fails later, where share is first used. evaluate x, from Control.Exception, is an action that computes x when it runs, so the error appears at that line.
unsafePerformIO. unsafePerformIO, from System.IO.Unsafe, turns an action into a plain value. The action then runs when that value is first needed: once, never, or at a point that no line of main shows. Keep the action in IO, and pass its result.
main :: IO ()
main = do
share <- return (div 120 0) :: IO Int
putStrLn "share ready" -- prints
print share -- *** Exception: divide by zero Run an action once and pass its result, check everything before the first write when the work is all or nothing, compute a value with evaluate where its error belongs, and keep actions out of plain values.
06 / Try it
Can you count how often an action runs?
Count how often sell runs. modifyIORef' sold (+ 1) adds 1 to what sold holds.
import Control.Monad (when)
import Data.IORef
main :: IO ()
main = do
sold <- newIORef (0 :: Int)
let sell = modifyIORef' sold (+ 1)
plan = [sell, sell, sell]
sell
sell
when (length plan > 5) sell
return ()
sequence_ (take 2 plan)
count <- readIORef sold
print count
What does the program print?
Reveal the reasoning
4. The two sell lines run sell twice, so sold holds 2. length plan is 3, so when runs nothing. return () runs, does nothing, and the next line still runs. take 2 plan is a list of two actions, and sequence_ runs both, so sold reaches 4. Building plan ran nothing, and its third sell never runs.
Practice tickets
- HS-701 · Sold-out dinosaur tours keep taking seats → Free
- HS-702 · New members get a card and a letter with different numbers → Sign in · Pro
- HS-703 · Specimen jar tags keep printing past a jar with no name → Sign in · Pro
Keep exploring
Errors as values or exceptions →