Chapter 2. Patterns for I/O

 

"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:

  • I/O as a first-class citizen
  • Imperative I/O
  • Lazy I/O
  • The problem with Lazy I/O
  • Resource management with bracket
  • Iteratee I/O

I/O as a first class citizen

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)

I/O as a functor, applicative, and monad

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset