Day 4 of 5
⏱ ~60 minutes
Haskell in 5 Days — Day 4

Monads & IO

Monads are Haskell's mechanism for sequencing computations with effects. The IO monad wraps all real-world interactions. Maybe and Either model computations that can fail. Today you demystify monads and write practical I/O programs.

The Maybe Monad

Maybe a is either Just a (success) or Nothing (failure). Chaining Maybe computations with >>= (bind) short-circuits on Nothing: 'lookup key m >>= lookup key2' — if the first lookup returns Nothing, the second never runs. Do-notation desugars to bind: 'do { x <- mx; y <- my; return (x+y) }' becomes 'mx >>= \x -> my >>= \y -> return (x+y)'. This eliminates null-check pyramids.

The IO Monad

IO a represents a computation that performs I/O and produces a value of type a. IO actions are values — combining them with >>= sequences their execution. 'main :: IO ()' is the entry point. Common actions: putStrLn, getLine, readFile, writeFile, hSetBuffering. IO keeps pure functions separate from side effects: a function that returns IO Double announces it has side effects; a function returning Double is guaranteed pure.

Either and Error Handling

Either e a represents Right a (success) or Left e (error with value of type e). Use it instead of exceptions for expected failures. 'parseAge :: String -> Either String Int' returns Left 'not a number' or Right 42. The Either monad short-circuits on Left, just like Maybe on Nothing. 'ExceptT' from transformers stacks Either with IO for programs with both effects.

haskell
import System.IO
import Data.Maybe (mapMaybe)
import Text.Read (readMaybe)

-- Safe integer parsing
safeParseInt :: String -> Maybe Int
safeParseInt = readMaybe

-- Chain Maybe with do-notation
lookupAndDouble :: String -> [(String, Int)] -> Maybe Int
lookupAndDouble key db = do
  val    <- lookup key db
  double <- safeParseInt (show (val * 2))
  return double

-- IO: interactive sum calculator
main :: IO ()
main = do
  hSetBuffering stdout LineBuffering
  putStrLn "Enter numbers (blank to finish):"
  nums <- collectNums
  putStrLn $ "Sum: " ++ show (sum nums)
  putStrLn $ "Count: " ++ show (length nums)

collectNums :: IO [Int]
collectNums = do
  line <- getLine
  if null line
    then return []
    else case readMaybe line :: Maybe Int of
      Nothing -> do putStrLn "Not a number, skipping."
                    collectNums
      Just n  -> do rest <- collectNums
                    return (n : rest)
💡
Think of do-notation as a recipe: each line performs an action and names its result. The IO monad ensures these actions execute in order. Without IO, Haskell code has no defined evaluation order.
📝 Day 4 Exercise
Write an Interactive Haskell Program
  1. Write a program that reads lines from stdin until EOF and prints the longest line
  2. Add error handling: if a line exceeds 1000 characters, print a warning and skip it
  3. Extend to read from a file: take the filename as a command-line argument
  4. Use Data.Map.Strict to count word frequencies in the file
  5. Print the top 10 most frequent words with their counts

Day 4 Summary

  • Maybe models optional values; >>= chains computations that might fail
  • Do-notation is syntactic sugar for >>= and >> (sequencing without binding)
  • IO a wraps side-effectful computations, keeping pure functions visibly pure
  • Either e a represents success (Right) or failure with error info (Left)
  • mapMaybe filters and transforms a list, discarding Nothing results
Challenge

Write a CSV parser in Haskell that handles quoted fields with embedded commas. Return Either String [[String]] — Left for parse errors, Right for a 2D grid of fields. Test it on a 10,000-row CSV file.

Finished this lesson?