Reading — step 1 of 7
Learn
Real Haskell code mixes pure logic with IO. The discipline: keep pure functions pure, isolate side effects to thin shells. Real World Haskell calls this "functional core, imperative shell."
Extracting from IO with <-
main :: IO ()
main = do
line <- getLine -- IO String, extracts to String
let processed = process line -- pure transformation
putStrLn processed -- IO ()
<-extracts the value from an IO actionletfor pure bindings (no IO involved)
The shape: read input → call pure function → write output. Pure logic (process) is isolated and testable without IO.
Calling pure functions from IO
main :: IO ()
main = do
nums <- map read . words <$> getLine -- IO [Int]
print (sum nums) -- IO ()
<$>isfmap— applies a pure function inside IO without unwrappingmap read . wordsis a pure transformation:String -> [Int]
return — wrapping pure values
lookupUser :: Int -> IO User
lookupUser id = do
raw <- queryDB id
let user = parseUser raw
return user -- wraps in IO
return :: a -> IO a wraps a pure value into an IO action. Misleading name — it's NOT control flow like in C/Java. It's just "inject this pure value into the IO context."
Modern Haskell often uses pure (more general) instead of return.
bracket — guaranteed cleanup
For resources that need cleanup (files, sockets, locks), use bracket:
import Control.Exception (bracket)
import System.IO
main :: IO ()
main = do
bracket
(openFile "data.txt" ReadMode) -- acquire
hClose -- release (always)
(\h -> do -- use
content <- hGetContents h
putStrLn ("got " ++ show (length content) ++ " bytes")
)
bracket acquire release use — guarantees release runs even if use throws. Like try/finally in other languages, but baked into the type system.
withFile is a higher-level wrapper:
import System.IO (withFile, IOMode(..))
main :: IO ()
main = do
withFile "data.txt" ReadMode $ \h -> do
content <- hGetContents h
putStr content
The handle is auto-closed when the lambda exits.
try and catch — handling exceptions
import Control.Exception
main :: IO ()
main = do
result <- try (readFile "missing.txt") :: IO (Either IOException String)
case result of
Right contents -> putStrLn contents
Left e -> putStrLn ("failed: " ++ show e)
try :: IO a -> IO (Either SomeException a) — runs the action, catching exceptions into Either.
catch :: IO a -> (SomeException -> IO a) -> IO a — handler-style.
result <- readFile "data.txt" `catch` \(_ :: IOException) ->
return "" -- fallback on missing file
evaluate — force a thunk inside IO
import Control.Exception (evaluate)
main = do
let x = 1 `div` 0 -- pure thunk; not yet evaluated
-- x is fine here — Haskell is lazy
result <- try (evaluate x) :: IO (Either SomeException Int)
print result -- Left ...
evaluate forces a pure value to weak head normal form INSIDE IO. Useful when you want to catch pure exceptions (division by zero, head of empty list) — they normally only manifest when the value is used.
Functional core, imperative shell
Typical Haskell program shape:
main :: IO ()
main = do
-- shell: read input
contents <- readFile "input.txt"
-- core: pure transformation
let result = process contents
-- shell: write output
writeFile "output.txt" result
process :: String -> String
process = unlines . map transform . lines
where
transform = ... -- pure logic, easily tested
The pure core is unit-testable without IO. The IO shell is small and obvious. Same pattern as functional design in any language.
Common mistakes
returnconfused with C-style return — it's justpurefor IO. Doesn't end the function.- Forgetting
<-vslet—<-extracts from IO;letbinds a pure value. Mixing them confuses compilers and readers. unsafePerformIOfor convenience — almost always wrong. Breaks purity guarantees in subtle ways.- Reading huge files lazily then accidentally retaining the handle — combine
readFilewith side effects carefully; consider strict alternatives. - Catching all exceptions — usually a code smell. Pattern-match on specific exception types.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…