IO
The IO monad is Haskell’s mechanism for effectful computation — input, output, mutation, concurrency, and any operation that interacts with the world outside the program. The type system tracks IO explicitly: a value of type IO a is a description of an effectful computation that, when run, produces an a. Pure code cannot perform I/O without the IO monad’s permission; the segregation is one of Haskell’s distinguishing features and the foundation of the language’s reasoning guarantees.
This page covers the IO type, the principal IO operations, the do-notation for sequencing, file and console I/O, the relationship between IO and pure code, and lazy I/O and its hazards.
The IO type
A value of type IO a is conceptually an action — a description of effectful computation:
main :: IO () -- a top-level action that produces ()
putStrLn :: String -> IO () -- print a string and a newline
getLine :: IO String -- read a line from stdin
readFile :: FilePath -> IO String -- read a file
writeFile :: FilePath -> String -> IO () -- write a file
The IO type is parameterised by the result type. IO () is an action with no useful result (the unit value); IO String produces a String; IO Int produces an Int.
A value of type IO a is not an a. The IO wrapper is the type-level mark that the value involves I/O; pure code cannot inspect or unwrap it. The only way to get the a out is to be in another IO context (the do-notation’s <- operator).
-- Pure:
greet :: String -> String
greet name = "Hello, " ++ name
-- Effectful (in IO):
main :: IO ()
main = do
name <- getLine -- name :: String
putStrLn (greet name)
main as the entry point
A Haskell program’s entry point is main, which must have type IO ():
main :: IO ()
main = putStrLn "Hello, world!"
The runtime invokes main to start the program; the actions described by main are then executed.
The do-notation
The do-notation sequences IO actions:
main :: IO ()
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Hello, " ++ name)
The block contains a sequence of statements, each an IO action. The <- binds the result of an action to a name; the next line in the block runs after the previous one.
The do-notation desugars to >>= (monadic bind):
main = putStrLn "What is your name?"
>> getLine
>>= \name -> putStrLn ("Hello, " ++ name)
The full treatment of >>= and the monad type class is in Monads.
The let admits pure bindings within a do-block:
main = do
line <- getLine
let upper = map toUpper line
len = length upper
putStrLn upper
print len
The let introduces pure bindings; no <- is needed because no effectful action is performed.
Console I/O
The principal console operations:
putStr :: String -> IO () -- print without newline
putStrLn :: String -> IO () -- print with newline
print :: Show a => a -> IO () -- = putStrLn . show
getLine :: IO String -- read a line
readLn :: Read a => IO a -- read and parse
getContents :: IO String -- read entire stdin
interact :: (String -> String) -> IO () -- transform stdin to stdout
getContents reads all of stdin lazily; the contents are produced as the program reads them. interact is the conventional one-liner for stdin-to-stdout filters:
main :: IO ()
main = interact (unlines . map reverse . lines)
-- reverses every line of input
The implementation of interact is getContents >>= putStr . f; the laziness admits processing arbitrarily large inputs.
File I/O
The conventional file operations:
readFile :: FilePath -> IO String -- read entire file (lazy!)
writeFile :: FilePath -> String -> IO () -- write entire file
appendFile :: FilePath -> String -> IO () -- append
readFile returns lazily — the file is held open until the contents are consumed. The mechanism admits processing very large files but introduces the lazy I/O hazard (treated below).
For more control, the Handle-based API in System.IO:
import System.IO
main = do
handle <- openFile "input.txt" ReadMode
contents <- hGetContents handle
-- ... process contents ...
hClose handle
The bracket pattern from Control.Exception admits exception-safe cleanup:
import Control.Exception
import System.IO
main = bracket
(openFile "input.txt" ReadMode)
hClose
(\handle -> do
contents <- hGetContents handle
process contents)
The conventional contemporary advice is to use withFile from System.IO:
import System.IO
main = withFile "input.txt" ReadMode $ \handle -> do
contents <- hGetContents handle
process contents
withFile opens the file, runs the action, and closes the file (even on exception). The pattern is the conventional Haskell idiom for file I/O.
For Text and ByteString I/O:
import qualified Data.Text.IO as TIO
import qualified Data.ByteString as BS
text <- TIO.readFile "input.txt"
bytes <- BS.readFile "input.bin"
For most non-trivial I/O, Data.Text.IO (with strict Text) is preferable to readFile (with String). The text package’s I/O routines also admit explicit charsets:
import qualified Data.Text.IO as TIO
import qualified Data.Text.Encoding as TE
import qualified Data.ByteString as BS
text <- TIO.readFile "utf8.txt" -- assumes UTF-8
-- Or with explicit encoding:
bytes <- BS.readFile "data.bin"
let text = TE.decodeUtf8 bytes
Handle and the I/O abstractions
A Handle represents an open file or stream:
import System.IO
stdin :: Handle
stdout :: Handle
stderr :: Handle
hGetLine :: Handle -> IO String
hPutStr :: Handle -> String -> IO ()
hPutStrLn :: Handle -> String -> IO ()
hClose :: Handle -> IO ()
hIsEOF :: Handle -> IO Bool
hFlush :: Handle -> IO ()
hGetContents :: Handle -> IO String -- lazy read of the rest
The standard handles stdin, stdout, stderr are pre-opened. The conventional patterns for explicit handles:
import System.IO
main = do
handle <- openFile "log.txt" AppendMode
hPutStrLn handle "an entry"
hClose handle
-- Or with bracket:
withFile "log.txt" AppendMode $ \h -> hPutStrLn h "an entry"
The withFile form is the conventional contemporary choice; it guarantees the file is closed.
Modes and buffering
openFile admits several modes:
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
openFile :: FilePath -> IOMode -> IO Handle
Buffering may be controlled:
hSetBuffering :: Handle -> BufferMode -> IO ()
data BufferMode
= NoBuffering
| LineBuffering
| BlockBuffering (Maybe Int)
The conventional discipline is to leave the default buffering alone unless measurement justifies changing it.
IORef for mutable references
Haskell admits mutable references in IO through IORef:
import Data.IORef
newIORef :: a -> IO (IORef a)
readIORef :: IORef a -> IO a
writeIORef :: IORef a -> a -> IO ()
modifyIORef :: IORef a -> (a -> a) -> IO ()
modifyIORef' :: IORef a -> (a -> a) -> IO () -- strict variant
main = do
ref <- newIORef 0
modifyIORef' ref (+ 1)
n <- readIORef ref
print n
IORef admits mutable state in IO; it is the conventional choice for stateful IO operations.
For atomic updates: atomicModifyIORef, atomicModifyIORef'. For thread-safe shared state across threads: MVar, TVar (treated in Concurrency).
The conventional advice is to prefer the State monad in pure code (treated in Monads) and IORef only when the state is genuinely tied to IO operations.
Lazy I/O and its hazards
The readFile and getContents functions return lazily — the contents are produced as they are consumed. The mechanism admits processing arbitrarily large files in constant memory:
main = do
contents <- readFile "huge.txt"
let firstLine = head (lines contents)
putStrLn firstLine
-- only the first line is read; the rest of the file is not loaded
The mechanism has substantial hazards:
File handles held open
main = do
contents <- readFile "log.txt"
-- ... process some of contents ...
return ()
-- the file may be held open until contents is GC'd
The handle is held until the lazy String is fully consumed (or garbage-collected). The mechanism produces “too many open files” errors in long-running programs that read many files.
Race conditions with concurrent file modification
contents <- readFile "data.txt"
-- if data.txt is modified concurrently, contents may reflect a mix
-- of old and new content
Order-of-evaluation surprises
main = do
contents <- readFile "input.txt"
writeFile "output.txt" (reverse contents)
-- The two operations may interact in unexpected ways:
-- if input.txt and output.txt are the same file, the result is undefined.
The conventional defences
- Use strict I/O — read the entire file into a strict
TextorByteString:
import qualified Data.Text.IO as TIO
import qualified Data.ByteString as BS
contents <- TIO.readFile "input.txt" -- strict; entire file loaded
bytes <- BS.readFile "input.bin" -- strict
- Use the
Handle-based API with explicit reading:
withFile "input.txt" ReadMode $ \h -> do
line1 <- hGetLine h
line2 <- hGetLine h
process line1 line2
-
Use
hCloseexplicitly when not usingwithFileorbracket. -
For streaming, use
conduitorpipeslibraries — they provide explicit-ordering streaming abstractions that avoid the lazy-I/O hazards.
The relationship between IO and pure code
A function with type String -> Int is pure: it cannot perform I/O. A function with type String -> IO Int is effectful: it may perform I/O.
The type system enforces the separation:
greet :: String -> String -- pure
greet name = "Hello, " ++ name
main :: IO () -- effectful (in IO)
main = do
name <- getLine -- bind effectful result
putStrLn (greet name) -- pass to pure function
Pure functions can be called from IO; IO actions cannot be called from pure code without being lifted to IO. The mechanism admits substantial benefit:
- Pure code is testable — same inputs yield same outputs; no fixtures, no mocks.
- Pure code is compositional — functions compose freely.
- The type signature documents effects — a function’s type tells whether it performs I/O.
The conventional Haskell discipline is to write the substantial majority of code as pure functions and to push I/O to the boundaries of the program. The IO-typed code typically reads input, applies a pure transformation, and writes output:
main = do
input <- readFile "input.txt"
let result = process input -- the actual work, pure
writeFile "output.txt" result
The pattern is the conventional Haskell architecture for batch programs.
Common patterns
Read, process, write
main = do
contents <- readFile "input.txt"
let processed = map toUpper contents
writeFile "output.txt" processed
Iterate over IO actions
main = do
forM_ [1..10] $ \n -> do
putStrLn ("Item " ++ show n)
process n
forM_ (and mapM_) admit iterating effectfully over a list, discarding the results. The _-suffixed forms are conventional for “for side effect only”.
Collect IO results
main = do
contents <- mapM readFile ["a.txt", "b.txt", "c.txt"]
print (length contents) -- 3
mapM collects the results into a list; mapM_ discards them.
Conditional IO
import Control.Monad (when, unless)
main = do
args <- getArgs
when (length args == 0) $ do
putStrLn "usage: prog <file>"
exitFailure
let file = head args
contents <- readFile file
process contents
when/unless admit conditional effects without an explicit if/then/else returning () and ().
Try-catch (exception handling)
import Control.Exception
main = do
result <- try (readFile "input.txt") :: IO (Either IOException String)
case result of
Right contents -> process contents
Left exception -> putStrLn ("Error: " ++ show exception)
The full treatment of exceptions is in Error handling.
Resource cleanup
import Control.Exception (bracket)
import System.IO
main = bracket
(openFile "log.txt" AppendMode)
hClose
(\h -> do
hPutStrLn h "an entry"
hPutStrLn h "another entry")
bracket ensures the resource is released even on exception. withFile is bracket for files.
IORef for stateful loops
import Data.IORef
main = do
counter <- newIORef 0
forM_ [1..1000] $ \_ -> do
modifyIORef' counter (+ 1)
n <- readIORef counter
print n
The pattern admits explicit accumulators in IO contexts; modern alternatives include State over IO via StateT.
Async and parallel I/O
For concurrent I/O, the async library:
import Control.Concurrent.Async
main = do
a1 <- async (fetch "url1")
a2 <- async (fetch "url2")
a3 <- async (fetch "url3")
[r1, r2, r3] <- mapM wait [a1, a2, a3]
-- or:
[r1, r2, r3] <- mapConcurrently fetch ["url1", "url2", "url3"]
The full treatment is in Concurrency.
A note on the IO discipline
The IO monad is one of Haskell’s most distinctive features. The conventional contemporary advice:
- Use strict
TextandByteStringI/O for non-trivial work; lazy I/O is convenient but produces hazards. - Use
withFile/bracketfor resource management; never rely on the GC for file handles. - Push pure transformations into pure functions;
IO-typed code should be the boundary, not the body. - Use
mapM/forM/mapM_/forM_for effectful iteration. - For substantial concurrent I/O, the
asynclibrary is the conventional choice.
The discipline of writing IO-correct Haskell is the discipline of segregating pure from effectful code, managing resources explicitly, and using the type system to track which operations have side effects. The combination is one of the language’s principal contributions to programming.