IO and purity
| IO is a type | |
| Actions, not statements | |
| Combining actions | |
| Keeping the middle pure |
IO is a type
An action that talks to the world has a type that says so. IO a means a program fragment that, when run, produces an a and may have done something outside:
getLine :: IO String
putStrLn :: String -> IO ()
readFile :: FilePath -> IO String
A value of type IO String is a description of an action, not the string itself. Nothing happens until the runtime runs it, which is why a pure function cannot perform IO by accident.
Actions, not statements
The do block from the last lesson is how actions are sequenced. Each line is a value, and <- takes the result out:
main :: IO ()
main = do
putStrLn "Name?"
name <- getLine
putStrLn ("Hello, " ++ name)
main is special: it is the action the runtime executes. Every other IO value is inert until something runs it.
Combining actions
Two combinators cover most loops and conditionals over actions. mapM_ runs an action for each element and discards the results, and when runs one conditionally:
mapM_ putStrLn ["a", "b"]
import Control.Monad (when)
main = do
let debug = True
when debug (putStrLn "starting")
mapM keeps the results in a list, and forM_ is the same loop with the arguments flipped so that the body comes last.
Keeping the middle pure
The useful structure of an IO program is a thin impure shell around a pure core. Read the input, hand it to pure functions, print the result:
-- pure, testable without a terminal
shout :: String -> String
shout = map toUpper
-- impure, three lines, no logic
main = do
line <- getLine
putStrLn (shout line)
Every hour spent moving logic out of do
blocks pays back in tests that need no mocking. The next lesson reads
and writes real files with the same split.
Next:
Files and IO
Article author: Arthur Isaev