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, givingdata Expr = Lit Integer | Var Var | Op Ops Expr Expr data Ops = Add | Sub | Mul | Div | Mod type Var = CharWe 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 isinitial :: 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 | Nulland defined the ingredients of the function
commLine :: String -> Commandwhich 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.