Introduction to Programming, Aug-Dec 2006 Lecture 13, Tuesday 10 Oct 2006 Lecture 14, Thursday 12 Oct 2006 Input/Output ------------ So far, we have invoked Haskell functions interactively, via the Haskell interpreter. In this mode of operation, there is no need for a Haskell program to interact with the "outside world". However, invoking functions interactively has its limitations. It is tedious to type in large inputs manually and read off voluminous output from the screen. Instead, it would be much more convenient to be able to read and write data to files. There is also a natural need for programs to function offline, without direct interaction from the user. Such programs also need mechanisms to take inputs from the environment and write their output back. Why is Input/Output an issue in Haskell? ---------------------------------------- In Haskell, computation is rewriting, using function definitions to simplify expressions. This rewriting is done lazily --- that is, the arguments to a function are evaluated only when needed. A highly desirable goal is confluence --- the order of evaluation of independent subexpressions should not affect the outcome of the computation. An obvious approach would be to make input and output operations functions. For instance, suppose we have a function "read" that reads an integer from the keyboard. Consider the following expression that reads two integers and computes their difference: difference = read - read An immediate problem with this expression is confluence: the order of evaluation of the two (independent) occurrences of read changes the value computed. There is a more subtle problem, arising out of lazy evaluation. Consider a list of the form [7, factorial 8, 3+5]. We can extract its length through the expression: length [factorial 8, 7, 3+5] The function length only needs to check the number of elements in the list, and not the actual values. Under Haskell's lazy evaluation mechanism, this expression evaluates to 3 without actually computing "factorial 8" or "3+5". On the other hand, computing head [factorial 8, 7, 3+5] would result in evaluating "factorial 8", but not "3+5". Consider now, corresponding expressions length [read, read, read] and head [read, read, read] Using lazy evaluation, no values are actually read when evaluating the first expression! On the other hand, evaluating the second expression would read one value. This means that an expression that includes functions that perform input or output is not guaranteed to actually execute the operation. From these observations, we see that input/output actions need to be done in a specific order. Further, there should be no uncertainty as to whether such an action has been performed. Actions ------- To fix this problem, Haskell introduces a new quantity called an action. We can think of the world of a Haskell program as divided into two parts. There is the ideal world of "Values" that contains Ints, Floats, Chars and functions involving such quantities. This is the world that we have been dealing with so far. Side by side with this world is the "real" world with a keyboard, screen, data files ... Actions are used to transfer information from the real world to the ideal world and back. For this course, the only actions we deal with are those involving input and output. Recall that a function can be viewed as a black box with an input and an output. The inputs and outputs to a function are not to be confused with the input and output that we are trying to formalize. To avoid confusion, we will refer to the input of a function as its argument and the output of a function as its result. Here is our abstract view of a function. ----------------- Argument | | Result ---------->| |---------> | | ----------------- Both the argument and the result of a function lie in the abstract world of Values. Remember that the argument or result could itself be another function over Values. Actions, on the other hand, simultaneously interact both with the world of Values and the real world. Here is an abstract picture of an action ----------------- Argument | | Result ---------->| |---------> | /\ | Value world | /||\ | ................|.......||........|......................... Real world | || | -------||--------- \||/ \/ The vertical arrow that penetrates inside the box denoting the action represents the fact that an action transfers data between the Value world and the Real world. One might argue that we should be more careful and describe whether the data flows upwards or downwards in an action, or both ways. However, as we have observed above, the data that flows across this boundary is inherently sequential. An action that reads two data items and then writes one is different from an action that writes one data item between two reads. Hence, we cannot separate out the upward and downward data streams into two "channels" because, if we did so, we might lose information about the order in which reads and writes occur. Instead, we should think of the action as providing a single doorway between the Value world and the Real world through which data items pass one at a time, either upwards or downwards. For instance, an action that reads a character does not need an argument. It interacts with the real world to fetch a character and returns the character that is read as its result. In Haskell, the action that does all this is called getChar and has the follwing type: getChar :: IO Char The word IO indicates that this action performs input/output. There is no argument, only a result type. How about the symmetric action that takes a Char value as argument and prints it out. This action has an argument, but no result. Haskell has a type containing no values, denote "()". Using this, we can describe the type of putChar as: putChar :: Char -> IO () Here, the argument is a Char and the () indicates that the action returns nothing. Notice that we did not need to use () to describe the lack of an argument to getChar because it is legal for a function/action to have only a result (in the world of functions, such a quantity would be a constant that always returns a fixed value). However, we cannot write an expression that leaves out the result type, so we need to use the empty type () in this context. Notice also that a function that reads an argument and does not generate a result is completely useless --- what we do with such an entity? On the other hand, actions can be "one-sided" with respect to the world of Values because they perform something nontrivial with respect to the Real world. The occurrence of an action changes the state of the Real world. For instance, getChar consumes one character from the keyboard, leaving the input data pointing to the next character that has been typed. Similary, putChar produces a character on the screen. In the literature, this behaviour of updating the Real world while reading and generating values is referred to as a "side effect" of the function. Composing actions ----------------- In any nontrivial Haskell program, we compose simple functions to create more complex ones. When we compose functions, we set up a pipe feeding the result of one function as the argument to another one. It is natural to want to the same with actions. For instance, suppose we want to combine getChar and putChar to generate a complex action that reads a character from the keyboard and prints it out to the screen. In function notation, it would suffice to write putChar(getChar) However, this notation hides the fact that we are composing actions, not functions. For instance, we have to evaluate getChar before putChar, which is not the order that normal Haskell evaluation would choose. To get around these difficulties, Haskell provides an explicit operator, >>=, to compose actions. The complex action we are trying to define is written getChar >>= putChar This is to be interpreted as "first do getChar, then feed the result as the argument to putChar". As with functions, we can give this complex action a name. For instance, we can write echo = (getChar >>= putChar) What is the type of echo? The first part of echo is getChar, which does not require an argument. The last part of echo is putChar, which does not produce a result. The argument that putChar requires is supplied internally by getChar and is not a visible part of echo. Thus, echo has type echo :: IO () Seemingly, echo does nothing at all, since it does not require an argument and does not produce a result! What saves the day is the tag IO, which says that though echo has no visible effect in the Value world, it does perform some interaction with the Real world. What is the type of >>=? It takes two actions and connects the output of one to the input of the other. A generic action is of type (a -> IO b) where a and b stand for normal types in the Value world. We might guess that the type of >>= is (>>=) :: (a -> IO b) -> (b -> IO c) -> (a -> IO c) However, >>= is restricted to combining actions in which the first action has no argument, so the actual type of >>= is (>>=) :: IO a -> (a -> IO b) -> IO b Now, suppose we want to repeat echo --- that is, compose echo with itself. We could try echoTwice = (echo >>= echo) but we have a problem. We observed that echo does not require an argument and does not produce a result, so it is not accurate to talk of the result of the first echo action being fed as the argument to the second echo. What we need is an alternative composition operator that discards the result of the first action. This operator is called >> and is of type (>>) :: IO a -> IO b -> IO b Thus echoTwice = (echo >> echo) What if we want to read a character and print it twice? We want a complex action of the form getChar -----> putChar -----> putChar | | ------------- We can achieve this by writing, first put2char :: Char -> IO () put2char c = (putChar c) >> (putChar c) and then writing modifiedecho = (getChar >>= put2Char) do: an easier way to generate sequences of actions --------------------------------------------------- We have seen that composing actions generates, in general, a sequence of actions. In this sequence, the results of some actions may be passed as arguments to one or more later actions. Haskell provides a simple notation to describe such sequences, using the special word "do". For instance, do putChar c putChar c is a complex action that generates two putChars in a row. Thus, we can rewrite the function put2char as put2char :: Char -> IO () put2char = do putChar c putChar c Observe that the lines below do are indented in a systematic way. How do we capture the result of an earlier action to reuse as an argument later? The operator <- binds a variable to a value. Thus, we can write echo :: IO() echo = do c <- getChar putChar c In the first line, c is bound to result of getChar. This value is then used in the second line as an argument to putChar. We can now write getput2char directly using do notation as follows. getput2char :: IO () getput2char = do c <- getChar putChar c putChar c return: promoting Values to Actions ----------------------------------- Suppose we want to write a function that reads a character and checks if it is '\n', the character corresponding to a newline. The function we want has the following type isnewline :: IO Bool because it takes no argument, does some IO (reads a character) and generates a result of type Bool (was the character equal to '\n'?). Here is an attempt at writing isnewline. isnewline = do c <- getChar if (c == '\n') then ? else ?? At ? and ?? we have to generate a result of type Bool. Unfortunately, it is not enough at this point to just generate True or False, as follows. isnewline = do c <- getChar if (c == '\n') then True else False This is because we want the final item in the do to be an action of type IO Bool, not a value of type Bool. The function return allows us to promote a simple value to an action. This leads us to the following version of isnewline. isnewline = do c <- getChar return (c == '\n') Composing actions recursively ----------------------------- Suppose we want to read a line of characters, terminated by '\n', and return the line as a String. The function we want is getLine = IO String which clearly requires as sequence of getChar actions. Here is an inductive definition of getLine: getLine = read a character c if c is '\n' the line is empty, return "" else read the rest of the line as s return (c:s) The actual definition is pretty much the same: getLine = do c <- getChar if (c == '\n') return "" else cs <- getLine return (c:cs) Notice the recursive call to getLine within the do. Actions are like values ----------------------- Actions can be thought of as special types of functions. Thus, just as we can use functions in place of simple types --- for instance, we can construct lists of functions and pass a function as an argument or obtain a function as a resutl --- we can use actions like simple types. Here is a list of actions of type [IO ()] [ putChar 'c', putChar 'z', echo ] We can write, for instance, a function that takes a list of actions and executes them as a sequence: dolist :: [IO ()] -> IO () dolist [] = return () dolist (c:cs) = do c dolist cs Haskell has a builtin function sequence of the following type: sequence :: [IO a] -> IO [a] In other words, sequence combines the results of a list of actions into a single list. Here is how sequence is defined: sequence [] = return [] sequence (c:cs) = do r <- c rs <- sequence cs return (r:rs) Notice the similarity in structure between sequence and getLine. getLine = do c <- getChar ------------------ | if (c == '\n') | | return "" | | else | ------------------ cs <- getLine return (c:cs) This is not surprising, since getLine combines the result of a sequence of getChar's into a list of Char, or String. The only difference is that the list of actions in getLine is terminated by reading '\n', so there is a condition to be checked before making a recursive call to itself. Using the Haskell compiler -------------------------- One of the standard Haskell compilers is the Glasgow Haskell Compiler which can be invoked using the command ghc. When you use an interpreter, you interact directly and can choose the function you want to evaluate. A compiled program runs autonomously, so there has to be an unambiguous way of specifying where the computation should start. Like many other languages, ghc expects computation to start with a function called main of type IO(), located in a module Main. One useful way to organize Haskell code is to put the actual code in a separate module and use main in module Main to just call the relevant function and print out its result using the builtin function. print :: Show a => a -> IO () For instance, suppose all our code is in a module called MyModule and the function to be invoked in MyModule is mymainfunction. Then, the module Main would look like the following: module Main where import MyModule main = print (mymainfunction) How do we actually compile the file? The command is ghc --make Main.hs -o outputfilename In this command, ghc is the name of the compiler while Main.hs is the module to compile. The flag "--make" tells ghc to look up and compile all modules referred to and required by Main.hs. The flag "-o" is used to specify the name of the final executable command. If this is left out, the default is to produce an executable called a.out. Exception handling ------------------ One of the complications associated with input/output in any language is that interaction with the real world often produces unexpected problems. For instance, the programe may be asked to read from a nonexistent file, or the disk might become full when writing. A program that interacts over the network might find that the network connection has temporarily failed. Such problems are called exceptions and should be clearly differentiated from computational errors such as dividing by zero or trying to extract the head of an empty list. There is no sensible way to continue execution when such a computational error arises. However, an exception such as "file not found" can be dealt with by asking the user to supply an alternate file name. Like many other languages, Haskell provides a mechanism for exception handling. The idea is to bundle an external "exception handler" along with the main function that may "raise" (or "throw") an exception. If no exception arises, the main function executes normally and produces a result. If an exception arises, the exception is passed to the handler function, which takes suitable action. The main point to be kept in mind is that the exception handler is expected, in the best case, to restore the computation to its normal course. Thus, the type of the result produced by the exception handler should be identical to that of the original function so that, when composed with other functions, the function+exception handler combination looks the same as the function alone. Schematically, we have the following picture in mind: ------------------- Argument | -------- | Result -------------->|function|----------------> | -------- | | | | | | | exception | | | | | | | v | | | --------- | | | |exception| | | | | handler |--- | | --------- | ------------------- The argument is passed to the function. If the function terminates normally, it produces a result. If it terminates abnormally, information about the exception is passed to the handler. If the handler can recover from the exception, it produces a result whose type is compatible with the original function. Haskell provides a function "catch" to combine a function with an exception handler into a single entity. Before going into the details of catch, let us look at a concrete example. In the function getLine that we wrote above, we assume that each line of text is terminated by a '\n'. It is possible, however, that the last line of text terminates with an end of file marker, without an explicit '\n'. Thus, while reading till the end of the current line, getLine could encounter an exceptional situation where end of file is reached. How should we recover from this? If we assume that the program can recognize an end of file situation, a good strategy is to treat this as a pseudo "end of line" and return the characters read at the end as the last line of text. Since the actual reading of input is done by getChar, the end of file error will arise there. In getLine, we check whether the character returned by getChar is '\n' is newline. What we can do is to bundle getChar with an excepttion handler that generates a '\n' when an end of file error occurs. Since, getLine uses getChar as a blackbox to read the next character, it has no way of knowing whether the '\n' that getChar returns was genuinely supplied in the input or was inserted by the exception handler in response to an end of file error. Thus, our aim is to provide an exception handler for getChar. This handler takes an exception as its argument and generates a Char as its result. Haskell has a type IOError for exceptions that arise from input/output operations. Since getChar is of type IO Char, the exception handler we seek can be written as eofhandler :: IOError -> IO Char so that its result type is compatible with that of getChar. What eofhandler has to do is to check if the error it receives is indeed an end of file error --- any error that getChar encounters would be passed onto eofhandler, but only an end of file error can be dealt with in the way we have described. The function isEOFError :: IOError -> Bool can be used to test whether the argument passed to eofhandler is an end of file error. Note that we do not actually check the value of IOError explicitly but rely on an abstract predicate to let us know what type of error we have got. An alternative would be to make IOError a datatype that enumerated all possible IO errors. The main reason for not doing this is that some implementations may support different kinds of such errors, and datatypes in Haskell are not extensible. Although the predicate approach is more cumbersome to use, it is easy to add support for a new IO error by simply including an additional predicate. Here then is a definition for eofhandler. eofhandler :: IOError -> IO Char eofhandler e | isEOFError e = return '\n' Note the "return" to make sure that the result type is IO Char and not just Char. Note also that eofhandler will itself generate an error if any other IOError is passed to it, since it can only respond to one case, where the argument is an EOFError. We will return to this point later. First, we return to catch. The function catch simply combines a function and its handler into a single unit. For instance, if we write getCharEOF = catch getChar eofhandler we get a function getCharEOF :: IO Char that can be used in place of getChar and that can convert end of file exceptions into '\n'. Thus, we can seamlessly replace getChar by getCharEOF in getLine as follows. getLine = do c <- getCharEOF if (c == '\n') return "" else cs <- getLine return (c:cs) The type of catch is catch :: IO a -> (IOError -> IO a) -> IO a In other words, catch combines a function and an exception handler to produce a new function with the same type as the original one. Let us get back to the question of what happens when getCharEOF receives an IOError other than an EOFError. As things stand, eofhandler will crash, saying "pattern match error". Instead, we can explicitly pass the error back up one level to the function that called getCharEOF using ioError, as follows. eofhandler :: IOError -> IO Char eofhandler e | isEOFError e = return '\n' | otherwise = ioError e The error passed on by ioError can be caught by getLine. For example, we could write getLinehandler :: IOError -> IO String getLinehandler e = return ("Error: "++(show e)) and getLinenew = catch getLine getLinehandler In this way, each exception or error can be caught and passed on to a handler associated with the function where it occurs. The handler can either explicitly deal with the error, pass it on or ignore it. If the handler ignores the error, the program terminates. If the handler passes it on, the error propogates one level up. Ultimately, the error will reach the top level. We can assume that there is an implicit error hander at the top level that prints out information about the error and terminates the program. This is the error message we see when a program fails. Reading and writing files ------------------------- In principle, reading from and writing to a file is no different from keyboard input and screen output. We just need to supply an extra parameter, namely the file name. In Haskell, a file name (or a fully qualified path) is just a String, but for the sake of abstractness, Haskell uses the type definition type FilePath = String We could then conceive of a function getCharFile that reads a character from a file with type getCharFile :: FilePath -> IO Char In other words, getCharFile takes the file name as its argument and returns the next character from that file. In practice, however, invoking a file explicitly when we read or write is very inefficient. If we supply the file name with getChar and putChar, each time we read or write a character, we have to open and close the file. Operating systems incur some overhead with opening and closing files so it is better not to repeatedly do this. Instead, we set up a connection to a file through a device called a "handle". When we want to use a file, we first open it and associate with it a handle. We then perform our input/output with respect to the handle. Finally, when we are done, we close the file and discard the handle. Opening a file requires the file name and the mode of opening the file. The different modes in which a file can be opened are given by an enumerated type, whose values are self-explanatory. data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode Here, then, is the function to open a file. openFile :: FilePath -> IOMode -> IO Handle When we open a file, we get back a Handle. We can then use the functions hGetChar and hPutChar to read and write characters via the given Handle (the h at the beginning of the function name indicates that the function works with respect to handles). Here are the types of hGetChar and hPutChar. hGetChar :: Handle -> IO Char hPutChar :: Handle -> Char -> IO() There are also functions hGetLine :: Handle -> IO String, and hPutStr :: Handle -> String -> IO() that read a line from a file and write a string to a file, respectively. Note that hPutStr does not automatically insert '\n' to signify the end of a line --- you have to put this in explicitly wherever you want a line to end. Finally, hClose :: Handle -> IO () closes a file. There is also a function hGetContents :: Handle -> IO String that reads in the entire text from a file as a String. This is done lazily, so a function such as copyfile :: Handle -> Handle copyfile fromhandle tohandle = do s <- hGetContents fromhandle hPutStr tohandle s will not necessarily read in the entire file associate with fromhandle. Typically, if its argument s is beyond a certain limit, hPutStr will write out its argument in blocks, generating a fixed amount of text at time. What happens is that hGetContents reads the contents of fromhandle in blocks corresponding to the way hPutStr writes out its values. Here is now a more elaborate program that reads two file names from the keyboard and copies the contents of the first file into the second one. putStr is a builtin function that prints a String to the screen. main = do fromhandle <- getAndOpenFile "Copy from: " ReadMode tohandle <- getAndOpenFile "Copy to: " WriteMode copyfile fromhandle tohandle hClose fromhandld hClose tohandle putStr "Done." getAndOpenFile :: String -> IOMode -> IO Handle getAndOpenFile prompt mode = do putStr prompt name <- getLine catch (openFile name mode) openhandler openhandler :: (String -> IOMode -> IO Handle) -> (IOError -> String -> IOMode -> IO Handle) -> (String -> IOMode -> IO Handle) openhandler e = do putStr ("Cannot open "++ name ++ "\n") getAndOpenFile prompt mode getAndOpenFile reads a file name and opens it with the appropriate mode. If opening the file generates an error, openhandler prints an error messge and repeats the process of asking for a filename to open. ======================================================================