Beautiful Code [267]
[||] You may think it odd that there are three function arrows in this type signature, rather than one. That's because Haskell supports currying, which you can find described in any book about Haskell (Haskell: The Craft of Functional Programming, by S.J. Thompson [Addison-Wesley]), or on Wikipedia. For the purposes of this chapter, simply treat all the types except the final one as arguments.
A side effect is anything that reads or writes mutable state. Input/output is a prominent example of a side effect. For example, here are the signatures of two Haskell functions with input/output effects:
hPutStr :: Handle -> String -> IO ()
hGetLine :: Handle -> IO String
We call any value of type IO t an action. So, (hPutStrh "hello") is an action[#] that, when performed, will print hello on handle[**]h and return the unit value. Similarly, (hGetLine h) is an action that, when performed, will read a line of input from handle h and return it as a String. We can glue together little side-effecting programs to make bigger side-effecting programs using Haskell's do notation. For example, EchoLine reads a string from the input and prints it:
[#]In Haskell, we write function application using simple juxtaposition. In most languages you would write hPutStr(h, "hello"), but in Haskell you write simply (hPutStrh "hello").
[**] A Handle in Haskell plays the role of a file descriptor in C: it says which file or pipe to read or write. As in Unix, there are three predefined handles: stdin, stdout, and stderr.
hEchoLine :: Handle -> IO String
hEchoLine h = do { s <- hGetLine h
; hPutStr h ("I just read: " ++ s ++ "\n")
; return s }
The notation do {a1; …; an} constructs an action by gluing together the smaller actions a1…an in sequence. So hEchoLine h is an action that, when performed, will first perform hGetLine h to read a line from h, naming the result s. Then it will perform hPutStr to print s, preceded []by "I just read: ". Finally, it will return the string s. This last line is interesting because return is not a built-in language construct: rather, it is a perfectly ordinary function with type:
[] The ++ operator concatenates two strings.
return :: a -> IO a
The action return v, when performed, returns v without having caused any side effects. []. This function works on values of any type, and we indicate this by using a type variable a in its type.
[] The IO type indicates the possibility of side effects, not the certainty
Input/output is one important sort of side effect. Another is the act of reading or writing a mutable variable. For example, here is a function that increments the value of a mutable variable:
incRef :: IORef Int -> IO ( )
incRef var = do { val <- readIORef var
; writeIORef var (val+1) }
Here, incRef var is an action that first performs readIORef var to read the value of the variable, naming its value val, and then performs writeIORef to write the value (val+1) into the variable. The types of readIORef and writeIORef are as follows:
readIORef :: IORef a -> IO a
writeIORef :: IORef a -> a -> IO ( )
A value of type IORef t should be thought of as a pointer, or reference, to a mutable location containing a value of type t, a bit like the type (t*) in C. In the case of incRef, the argument has type IORef Int because incRef applies only to locations that contain an Int.
So far, I have explained how to build big actions by combining smaller ones together—but how does an action ever actually get performed? In Haskell, the whole program defines a single IO action, called main. To run the program is to perform the action main. For example, here is a complete program:
main :: IO ( )
main = do { hPutStr stdout "Hello"
; hPutStr stdout " world\n" }
This program is a sequential program because the do notation combines IO actions in sequence. To construct a concurrent program we need one more primitive, forkIO:
forkIO :: IO a -> IO ThreadId
The function forkIO, which is built into Haskell, takes an IO action as its argument, and