"I believe that the monadic approach to programming, in which actions are first class values, is itself interesting, beautiful and modular. In short, Haskell is the world's finest imperative programming language" | ||
--Tackling the Awkward Squad, Simon Peyton Jones |
It is remarkable that we can do side-effecting I/O in a pure functional language!
We start this chapter by establishing I/O as a first-class citizen of Haskell. The bulk of this chapter is concerned with exploring three styles of I/O programming in Haskell.
We start with the most naïve style: imperative style. From there, we move on to the elegant and concise "lazy I/O", only to run into its severe limitations. The way out is the third and last style we explore: iteratee I/O.
As a binding thread, we carry a simple I/O example through all three styles. We will cover the following topics:
The IO Monad provides the context in which side effects may occur, and it also allows us to decouple pure code from I/O code. In this way, side effects are isolated and made explicit. Let's explore the ways in which I/O participates as a first-class citizen of the language:
import System.IO import Control.Monad import Control.Applicative main = do h <- openFile "jabberwocky.txt" ReadMode line <- hGetLine h putStrLn . show . words $ line hClose h
This code looks imperative in style: it seems as if we are assigning values to h
and line
, reading from a file, and then leaving side effects with the putStrLn
function.
The openFile
and hGetLine
functions are I/O actions that return a file handle and string, respectively:
openFile :: FilePath -> IOMode -> IO Handle hGetLine :: Handle -> IO String
The hClose
and putStrLn
functions are I/O actions that return nothing in particular:
putStrLn :: String -> IO () hClose :: Handle -> IO ()
In the putStrLn . show
function, we compose a function that returns an I/O action with a pure function:
(putStrLn :: String -> IO ()) . (show :: Show a => a -> String) putStrLn . show :: Show a => a -> IO ()
From this, we can see that functions can return I/O actions; functions can take I/O actions as arguments. We can compose regular functions with functions that return I/O actions. This is why it is said that I/O is a first-class citizen of Haskell.
Although do
is syntactic sugar for bind
, in a case like this, where there is just one "bind
", using the bind notation is more concise:
hGetLine h >>= print . words
-- vs
-- line <- hGetLine h
-- print . words $ line
(where print = putStrLn . show
)
So far, we have described I/O as a Monad
. In the next chapter, we will do an indepth survey of the hierarchy formed by the Functor
, Applicative
, and Monad
. For our purposes here, simply note that the I/O Monad is also an Applicative Functor
, which in turn is a Functor. I/O as Functor enables us to apply the (show . words)
function to the result of the hGetLineh
action:
line <- fmap (show . words) (hGetLine h)
putStrLn line
When we treat I/O as an Applicative Functor
we can use this syntax instead:
line <- (show . words) <$> (hGetLine h)
putStrLn line
For the monadic version of the preceding code, we use the liftM
function:
line <- liftM (show . words) (hGetLine h)
putStrLn line
All three styles of the fmap
function are equivalent in this case. However, as we will see, Monad
is more powerful than Applicative
, and Applicative is more powerful than Functor.