पाठ 13 / 25

IO and Exceptions

Perform input and output, handle exceptions and manage resources.

Effects at the edges

An IO a value is a description of an action that, when run by the runtime, may interact with the world and produce an a. The program's main :: IO () is the root action; IO actions are combined with do notation, and pure functions are called from inside them. A pure function cannot run an IO action, which is what keeps purity: effects live at the edges, and the core logic stays pure and easily testable. Common actions: putStrLn, getLine, readFile, writeFile, getArgs from System.Environment, and hFlush stdout. Use readMaybe instead of read for parsing input, since read crashes on bad input. Exceptions exist in IO (and can be thrown from pure code by partial functions such as head and div by zero). Handle them with Control.Exception: try returns an Either, catch installs a handler, throwIO raises, and custom exception types derive Show and declare instance Exception. Use bracket or the with... functions (withFile) to guarantee resource cleanup. Prefer Maybe and Either for expected failures and exceptions for I/O and truly unexpected errors.

Pure core, IO shell

Pure functions sit inside; IO actions at the boundary talk to the world.

A central circle containing small function boxes, surrounded by a ring with keyboard, file and network icons connected to it.
Figure 5.1 — Functional core, imperative shell.

Reading input, handling exceptions and using files safely

readMaybe for input, try for exceptions, withFile for resources.

import Control.Exception (Exception, IOException, throwIO, try)
import System.IO (IOMode (AppendMode), hFlush, hPutStrLn, stdout, withFile)
import Text.Read (readMaybe)

newtype InvalidQuantity = InvalidQuantity String
  deriving Show

instance Exception InvalidQuantity

askQuantity :: IO Int
askQuantity = do
  putStr "Quantity: "
  hFlush stdout
  line <- getLine
  case readMaybe line of
    Just n | n > 0 -> pure n
    _              -> throwIO (InvalidQuantity line)

main :: IO ()
main = do
  result <- try askQuantity
  case result of
    Left (InvalidQuantity raw) -> putStrLn ("Not a valid quantity: " ++ show raw)
    Right n                    -> putStrLn ("Ordering " ++ show n ++ " items")

  contents <- try (readFile "prices.txt") :: IO (Either IOException String)
  putStrLn $ either (\e -> "cannot read prices: " ++ show e)
                    (\c -> "read " ++ show (length (lines c)) ++ " price lines")
                    contents

  withFile "orders.log" AppendMode $ \h ->      -- the handle is closed even on exceptions
    hPutStrLn h "order placed"

read is partial

read "abc" :: Int throws an exception at run time. For user input, files and network data, always use readMaybe (or a proper parser) and handle the Nothing case.

त्वरित जाँच: How does Haskell keep functions pure while still doing I/O?

  • I/O is described by IO actions in the type, which pure functions cannot run
  • It forbids I/O
  • It uses global variables
  • All functions are impure
Answer

I/O is described by IO actions in the type, which pure functions cannot run — Effects are values of type IO, run only by the runtime from main.