Challenge 2: The Same Task, Using ExceptT String IO Int — Possible Solution ==================================================================== Main.hs: import Control.Monad.Trans.Except import Control.Monad.Trans.Class (lift) import Text.Read (readMaybe) readIntLineT :: ExceptT String IO Int readIntLineT = do line <- lift getLine case readMaybe line of Just n -> return n Nothing -> throwError "Not a valid integer" main :: IO () main = do result <- runExceptT readIntLineT case result of Right n -> putStrLn ("Parsed: " ++ show n) Left err -> putStrLn err Sample run (input: "42"): Parsed: 42 Sample run (input: "abc"): Not a valid integer Explanation: Inside readIntLineT's own do-block, `lift getLine` brings the plain IO action into the ExceptT context, and throwError short-circuits the WHOLE computation the same way Nothing did in Challenge 1's Maybe -- but now it happens inside ONE unified do-block, mixing the IO step and the error-handling step together, rather than needing a separate manual case for the inner layer the way Challenge 1 did. The remaining "unwrapping" only happens once, at the very outside, via runExceptT. Readability comparison: this version reads slightly more explicitly sequential -- the parse failure and the IO read live in the SAME do-block, rather than Challenge 1's separate getLine-then-case structure. The genuine cost, as the chapter is honest about, is the extra `lift` call needed for getLine, and the extra import/type machinery (ExceptT, runExceptT, throwError) that a beginner has to learn before this pattern becomes natural -- a real, non-trivial amount of new ceremony for what is still, ultimately, a fairly small program. WHY THIS WORKS AS AN ANSWER ------------------------------ This reimplements Challenge 1's exact task using ExceptT String IO Int with lift and throwError exactly as the chapter introduces them, and gives an honest, balanced comparison of the readability trade-off rather than claiming the transformer version is unambiguously better.