Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

I/O programming

This chapter looks again at I/O in Haskell, particularly focussing on the do notation which we use to write I/O programs. In the next chapter we’ll see that we can write all sorts of different programs using do notation, including programs that manipulate state, that non-deterministically return more than one result, as well as programs that may fail, or raise exceptions. Underlying each of these is a monad. A monad provides the infrastructure for sequencing and naming used in the do notation, and we will show instances of the Monad class for lists, the Maybe type, a parsing monad and more.

I/O programming

We first looked at I/O programming in Haskell using the IO types in Playing the game: I/O in Haskell. Something of type IO t is a program which will perform I/O and return a value of type t. Among the I/O primitives of Haskell are

putStr :: String -> IO ()
getLine :: IO String

The effect of running putStr str is to output the string str and then to terminate; it will return the value () which is (the only value) of type (). The effect of executing getLine is to get a line of input, and to return that line as its result.

Programs are built using the do notation to sequence statements of type IO t. A program to read a line then output a message is given by

readWrite :: IO ()

readWrite =
    do
      getLine
      putStrLn "one line read"

where the two statements are sequenced one after the other. It is also possible to name the results of statements for use in the program, so that we can name the line read, and output it, like this:

readEcho :: IO ()

readEcho =
    do
      line <-getLine
      putStrLn ("line read: " ++ line)

In this program line <- getLine names the result of the getLine and so it can be echoed in the next statement.

Adding a sequence of integers

Now suppose we want to write an interactive program to sum integers supplied one per line until zero is input. We will write an I/O program

sumInts :: Integer -> IO Integer

which is passed a starting value for the sum, and which returns the sum from there; so, to sum from scratch we call sumInts 0. This gives the program

sumInts s
  = do n <- getInt
       if n==0 
          then return s
          else sumInts (s+n)

In writing the program there are two cases: we first read an integer value, using the getInt function defined in Playing the game: I/O in Haskell. If we read zero then the result must be the sum so far, s; if not, we get the result by calling sumInts again, with the parameter set to (s+n).

The program is written in the style we saw in Playing the game: I/O in Haskell: the function is tail-recursive – being called as the last statement in the program – and the loop data is used to carry the “sum so far”: here s and s+n.

It is interesting with compare the definition of sumInts with the recursion in

sumAcc s [] = s
sumAcc s (n:ns) 
  = if n==0
       then s
       else sumAcc (s+n) ns

Here we use the first variable to sumAcc as an accumulator where the “result so far” can be held.

We can also put the sumInts program inside a ‘wrapper’ which explains its purpose and prints the sum at the end.

sumInteract :: IO ()
sumInteract
  = do putStrLn "Enter integers one per line"
       putStrLn "These will be summed until zero is entered"
       sum <- sumInts 0
       putStr "The sum is "
       print sum

Exercises

18.1 Compare the definition of sumInts above with this definition:

sumInts :: IO Integer
sumInts
  = do n <- getInt
       if n==0 
          then return 0
          else (do m <- sumInts
                   return (n+m))

Which will have the better performance, do you think? Wny?

18.2 Give a definition of the function

fmap :: (a -> b) -> IO a -> IO b

the effect of which is to transform an interaction by applying the function to its result. You should define it using the do construct.

18.3 Define the function

repeat ::  IO Bool -> IO () -> IO ()

so that repeat test oper has the effect of repeating oper until the condition test is True.

18.4 Define the higher-order function while in which the condition and the operation work over values of type a. Its type should be

whileG :: (a -> IO Bool) -> (a -> IO a) -> (a -> IO a)

18.5 Using the function whileG or otherwise, define an interaction which reads a number, n say, and then reads a further n numbers and finally returns their average.

18.6 Modify your answer to the previous question so that if the end of file is reached before n numbers have been read, a message to that effect is printed.

18.7 Define a function

accumulate :: [IO a] -> IO [a]

which performs a sequence of interactions and accumulates their result in a list. Also give a definition of the function

sequence :: [IO a] -> IO ()

which performs the interactions in turn, but discards their results. Finally, show how you would sequence a series, passing values from one to the next:

seqList :: [a -> IO a] -> a -> IO a

What will be the result on an empty list?

Further I/O

In this section we survey further features of Haskell I/O, defined in the System.IO module.

Interaction at the terminal

We have seen that we can read from the terminal – the ‘standard input’ – and write to the screen – the ‘standard output’. Terminal input can be configured to work in different ways, depending on the way that the input is buffered.

The default is that input is unbuffered, and so each character is available to the I/O program immediately it is typed; a disadvantage of this mode is that it is impossible to use Ctrl-D to signal “end of file” in the interactive input. Setting to line buffering, where input is assembled into lines before being available to the I/O program, remedies this problem. To set the buffering modes we use hSetBuffering as in this example:

copyInteract :: IO ()

copyInteract = 
    do
      hSetBuffering stdin LineBuffering
      copyEOF
      hSetBuffering stdin NoBuffering

where we can see that the buffering mode can be changed within an I/O program. The copyEOF program itself is given by

copyEOF :: IO ()

copyEOF = 
    do 
      eof <- isEOF
      if eof  
        then return () 
        else do line <- getLine 
                putStrLn line
                copyEOF

where isEOF :: IO Bool is an I/O program to determine whether or not the end of standard input has been reached.

Give this a try within GHCi: you will find that you can use Ctrl-D to terminate your input if you run copyInteract; if on the other hand you run copyEOF directly, you will need to interrupt the program using Ctrl-C.

File I/O

As well as reading and writing to a terminal, the Haskell I/O model also provides for reading from and writing and appending to files, by means of the functions

readFile   :: FilePath -> IO String
writeFile  :: FilePath -> String -> IO ()
appendFile :: FilePath -> String -> IO ()

where

type FilePath = String

and files are specified by the text strings appropriate to the implementation in question.

Errors

I/O programs can raise errors, which belong to the system-dependent data type IOError. The function

ioError :: IOError -> IO a

builds an I/O action which fails giving the appropriate error, and the program

catch :: IO a -> (IOError -> IO a) -> IO a

will catch an error raised by the first argument and handle it using the second argument, which gives a handler – that is an action of type IO a – for each possible IOError.

Input and output as lazy lists

An alternative view of I/O programs, popular in earlier lazy functional programming languages, was to see the input and output as Strings, that is as lists of characters. Under that model an I/O program is a function

listIOprog :: String -> String

This obviously makes sense in a ‘batch’ program, where all the input is read before any output is produced, but in fact it also works for interactive programs where input and output are interleaved, if the language is lazy. This is because in a lazy language we can begin to print the result of a computation – the output of the interactive program here – before the argument – the interactive input – is fully evaluated. As an example, repeatedly to reverse lines of input under this model one can write

listIOprog  = unlines . map reverse . lines

The drawback of this approach is in scaling it up. It is often difficult to predict in advance the way in which the input and output are interleaved: often output comes after it is expected, and sometimes even before; the IO approach in Haskell avoids such problems. Nevertheless, support for this style is available, using

getContents :: IO String

a primitive to get the contents of the standard input, and which is used in this function, also defined in System.IO

interact :: (String -> String) -> IO ()
interact f = do s <- getContents
                putStr (f s)

Try out the program interact listIOprog in GHCi to see for yourself that the input and output lines are interleaved one-by-one just as you would expect.

Generating random numbers

We introduced the randomStrategy in Playing the game: I/O in Haskell, but pointed out there that it should properly be programmed using a monad, since it is a function which returns different values on different calls. We generate a random integer less than n based on the current time, which is accessible within the IO monad:

randomInt :: Integer -> IO Integer
randomInt n = 
    do
      time <- getCurrentTime
      return ( (`rem` n)  read  take 6 $ 
                         formatTime defaultTimeLocale "%q" time)
      
randInt :: Integer -> Integer
randInt = unsafePerformIO . randomInt 
      
sRandom :: Strategy
sRandom _ = convertToMove (randInt 3)

Here we use the unsafePerformIO function to extract the result from the IO monad. As its name suggests this is (potentially) unsafe, and should be used with care; some would go further, and say that it should never be used.

If we want to avoid unsafePerformIO we need to look again at the design of our case study. In Playing the game: I/O in Haskell defined strategies to belong to the type

type Strategy = [Move] -> Move

if we want to include monadic functions, then we should redefine them to be monadic, like this

type StrategyM = [Move] -> IO Move

We leave the reimplementation of the case study to use monadic strategies as an exercise for the reader.

The System.IO library

There is much more to the System.IO library than we are able to cover here; see the system documentation for more details.

Exercises

18.8 Write a file-handling version of the program sumInts, which reads the integer sequence from a file.

18.9 Write a lazy-list version of the program sumInts, which you can then run using interact.

18.10 [Harder] Write a lazy-list version of the calculator program.

18.11 [Harder] Reimplement the Rock - Paper - Scissors case study to use the monadic strategies in StrategyM.

The calculator

The ingredients of the calculator are contained in three places in the text.

  • In Recursive algebraic types we saw the introduction of the algebraic type of expressions, Expr, which we subsequently revised in Case study: parsing expressions, giving

    data Expr = Lit Integer | Var Var | Op Ops Expr Expr
    data Ops  = Add | Sub | Mul | Div | Mod
    type Var  = Char
    

    We revise the evaluation of expressions after discussing the store below.

  • In Abstract data types we introduced the abstract type Store, which we use to model the values of the variables currently held. The signature of the abstract data type is

    initial :: Store 
    value   :: Store -> Var -> Integer
    update  :: Store -> Var -> Integer -> Store
    
  • In Case study: parsing expressions we looked at how to parse expressions and commands,

    data Command = Eval Expr | Assign Var Expr | Null
    

    and defined the ingredients of the function

    commLine :: String -> Command
    

    which is used to parse each line of input into a Command. For instance,

    commLine "(3+x)"   = (Eval (Op Add (Lit 3) (Var 'x')))
    commLine "x:(3+x)" = (Assign 'x' (Op Add (Lit 3) (Var 'x')))
    commLine "something unparsable" = Null
    

Expressions are evaluated by

eval :: Expr -> Store -> Integer

eval (Lit n) st = n
eval (Var v) st = value st v
eval (Op op e1 e2) st
  = opValue op v1 v2
    where
    v1 = eval e1 st
    v2 = eval e2 st

where the opValue function of type Ops->Integer->Integer->Integer interprets each operator, such as Add, as the corresponding function, like (+).

What is the effect of a command? An expression should return the value of the expression in the current store; an assignment will change the store, and a null command will do nothing. We therefore define a function which returns both a value and a store,

command :: Command -> Store -> (Integer,Store)

command Null st     = (0 , st)
command (Eval e) st = (eval e st , st)
command (Assign v e) st 
  = (val , newSt)
    where
    val   = eval e st
    newSt = update st v val

A single step of the calculator will take a starting Store, and read an input line, evaluate the command in the line, print some output and finally return an updated Store,

calcStep :: Store -> IO Store

calcStep st
  = do line <- getLine
       let comm        = commLine line  -- (1)
       let (val,newSt) = command comm st  -- (2)
       print val
       return newSt

In lines (1) and (2) of the definition of calcStep we see an example of the use of let within a do expression. Line (1),

let comm        = commLine line

gives comm the value of parsing the line, and this is subsequently used in (2),

let (val,newSt) = command comm st

which simultaneously gives val and newSt the value of the expression read and the new state. In the lines that follow the lets, the value val is printed and the new state newSt is returned as the overall result of the interaction. A sequence of calculator steps is given by

calcSteps :: Store -> IO ()

calcSteps st =
    do
      eof <- isEOF
      if eof
         then return ()
         else do newSt <- calcStep st
                 calcSteps newSt

The main I/O program for the calculator is given by starting off calcSteps with the initial store, operating in modified buffering mode

mainCalc :: IO ()
mainCalc = 
    do
      hSetBuffering stdin LineBuffering
      calcSteps initial
      hSetBuffering stdin NoBuffering

In the exercises various extensions and modifications of the calculator program are discussed.

Exercises

18.12 How would you add initial and final messages to the output of the calculator?

18.13 Discuss how you would have to modify the system to allow variables to have arbitrarily long names, consisting of letters and numbers, starting with a letter.

18.14 How would you extend the calculator to deal with decimal floating-point numbers as well as integers?

18.15 Discuss how you would modify the calculator so that it could read input commands split over more than one line. You will need to decide how this sort of split is signalled by the user – maybe by \ at the end of the line – and how to modify the interaction program to accommodate this. Alternatively, you might let the user do this without signalling; can you modify the program to do that?

18.16 How would you modify the parser so that ‘white space’ is permitted in the input commands, as in the example

"    x : (2\t+3)     "

which parses to the Command

(Assign 'x' (Op Add (Lit 2) (Lit 3)))

Summary

In this chapter we have seen more examples of how the do notation can be used for I/O programming. The mechanism which underlies the do notation is the Monad class, and we return to that in the next chapter.