The IO Library
Here, we'll explore the most commonly used elements of the
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode openFile :: FilePath -> IOMode -> IO Handle hClose :: Handle -> IO () hIsEOF :: Handle -> IO Bool hGetChar :: Handle -> IO Char hGetLine :: Handle -> IO String hGetContents :: Handle -> IO String getChar :: IO Char getLine :: IO String getContents :: IO String hPutChar :: Handle -> Char -> IO () hPutStr :: Handle -> String -> IO () hPutStrLn :: Handle -> String -> IO () putChar :: Char -> IO () putStr :: String -> IO () putStrLn :: String -> IO () readFile :: FilePath -> IO String writeFile :: FilePath -> String -> IO ()
Most of the IO functions are self-explanatory. The
hClose functions open and close a file, respectively. The
IOMode argument determines the mode for opening the file.
hIsEOF tests for end-of file.
hGetLine read a character or line (respectively) from a file.
hGetContents reads the entire file. The
getContents variants read from standard input.
hPutChar prints a character to a file;
hPutStr prints a string; and
hPutStrLn prints a string with a newline character at the end. The variants without the
h prefix work on standard output. The
writeFile functions read and write an entire file without having to open it first.
bracket function comes from the
Control.Exception module. It helps perform actions safely.
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
Consider a function that opens a file, writes a character to it, and then closes the file. When writing such a function, one needs to be careful to ensure that, if there were an error at some point, the file is still successfully closed. The
bracket function makes this easy. It takes three arguments: The first is the action to perform at the beginning. The second is the action to perform at the end, regardless of whether there's an error or not. The third is the action to perform in the middle, which might result in an error. For instance, our character-writing function might look like:
writeChar :: FilePath -> Char -> IO () writeChar fp c = bracket (openFile fp WriteMode) hClose (\h -> hPutChar h c)
This will open the file, write the character, and then close the file. However, if writing the character fails,
hClose will still be executed, and the exception will be reraised afterwards. That way, you don't need to worry too much about catching the exceptions and about closing all of your handles.
A File Reading Program
We can write a simple program that allows a user to read and write files. The interface is admittedly poor, and it does not catch all errors (such as reading a non-existent file). Nevertheless, it should give a fairly complete example of how to use IO. Enter the following code into "FileRead.hs," and compile/run:
module Main where import System.IO import Control.Exception main = doLoop doLoop = do putStrLn "Enter a command rFN wFN or q to quit:" command <- getLine case command of 'q':_ -> return () 'r':filename -> do putStrLn ("Reading " ++ filename) doRead filename doLoop 'w':filename -> do putStrLn ("Writing " ++ filename) doWrite filename doLoop _ -> doLoop doRead filename = bracket (openFile filename ReadMode) hClose (\h -> do contents <- hGetContents h putStrLn "The first 100 chars:" putStrLn (take 100 contents)) doWrite filename = do putStrLn "Enter text to go into the file:" contents <- getLine bracket (openFile filename WriteMode) hClose (\h -> hPutStrLn h contents)
What does this program do? First, it issues a short string of instructions and reads a command. It then performs a case switch on the command and checks first to see if the first character is a `q.' If it is, it returns a value of unit type.
If the first character of the command wasn't a `q,' the program checks to see if it was an 'r' followed by some string that is bound to the variable
filename. It then tells you that it's reading the file, does the read and runs
doLoop again. The check for `w' is nearly identical. Otherwise, it matches
_, the wildcard character, and loops to
doRead function uses the
bracket function to make sure there are no problems reading the file. It opens a file in
ReadMode, reads its contents and prints the first 100 characters (the
take function takes an integer and a list and returns the first elements of the list).
doWrite function asks for some text, reads it from the keyboard, and then writes it to the specified file.
The program has one major problem: it will die if you try to read a file that doesn't already exist or if you specify some bad filename like
*\bs^#_@. You may think that the calls to
doWrite should take care of this, but they don't. They only catch exceptions within the main body, not within the startup or shutdown functions (
hClose, in these cases). To make this completely reliable, we would need a way to catch exceptions raised by
Write a variation of our program so that it first asks whether the user wants to read from a file, write to a file, or quit. If the user responds with "quit", the program should exit. If they respond with "read", the program should ask them for a file name and then print that file to the screen (if the file doesn't exist, the program may crash). If they respond with "write", it should ask them for a file name and then ask them for text to write to the file, with "." signaling completion. All but the "." should be written to the file.
For example, running this program might produce:
Do you want to [read] a file, [write] a file, or [quit]? read Enter a file name to read: foo ...contents of foo... Do you want to [read] a file, [write] a file, or [quit]? write Enter a file name to write: foo Enter text (dot on a line by itself to end): this is some text for foo . Do you want to [read] a file, [write] a file, or [quit]? read Enter a file name to read: foo this is some text for foo Do you want to [read] a file, [write] a file, or [quit]? blech I don't understand the command blech. Do you want to [read] a file, [write] a file, or [quit]? quit Goodbye!