Learn You a Haskell for Great Good! - Miran Lipovaca [77]
dogaroo
radar
rotor
madam
This is what we get by redirecting it into our program:
$ ./palindrome < words.txt
not a palindrome
palindrome
palindrome
palindrome
Again, we get the same output as if we had run our program and put in the words ourselves at the standard input. We just don’t see the input that our program gets because that input came from the file.
So now you see how lazy I/O works and how we can use it to our advantage. You can just think in terms of what the output is supposed to be for some given input and write a function to do that transformation. In lazy I/O, nothing is eaten from the input until it absolutely must be, because what we want to print right now depends on that input.
Reading and Writing Files
So far, we’ve worked with I/O by printing stuff to the terminal and reading from it. But what about reading and writing files? Well, in a way, we’ve already been doing that.
One way to think about reading from the terminal is that it’s like reading from a (somewhat special) file. The same goes for writing to the terminal— it’s kind of like writing to a file. We can call these two files stdout and stdin, meaning standard output and standard input, respectively. Writing to and reading from files is very much like writing to the standard output and reading from the standard input.
We’ll start off with a really simple program that opens a file called girlfriend.txt, which contains a verse from Avril Lavigne’s hit song “Girlfriend,” and just prints out to the terminal. Here’s girlfriend.txt:
Hey! Hey! You! You!
I don't like your girlfriend!
No way! No way!
I think you need a new one!
And here’s our program:
import System.IO
main = do
handle <- openFile "girlfriend.txt" ReadMode
contents <- hGetContents handle
putStr contents
hClose handle
If we compile and run it, we get the expected result:
$ ./girlfriend
Hey! Hey! You! You!
I don't like your girlfriend!
No way! No way!
I think you need a new one!
Let’s go over this line by line. The first line is just four exclamations, to get our attention. In the second line, Avril tells us that she doesn’t like our current partner of the female persuasion. The third line serves to emphasize that disapproval, and the fourth line suggests we should go about finding a suitable replacement.
Let’s also go over the program line by line. Our program is several I/O actions glued together with a do block. In the first line of the do block is a new function called openFile. It has the following type signature:
openFile :: FilePath -> IOMode -> IO Handle
openFile takes a file path and an IOMode and returns an I/O action that will open a file and yield the file’s associated handle as its result. FilePath is just a type synonym for String, defined as follows:
type FilePath = String
IOMode is a type that’s defined like this:
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
Just like our type that represents the seven possible values for the days of the week, this type is an enumeration that represents what we want to do with our opened file. Notice that this type is IOMode and not IO Mode. IO Mode would be the type of I/O action that yields a value of some type Mode as its result. IOMode is just a simple enumeration.
Finally, openFile returns an I/O action that will open the specified file in the specified mode. If we bind that action’s result to something, we get a Handle, which represents where our file is. We’ll use that handle so we know which file to read from.
In the next line, we have a function called hGetContents. It takes a Handle, so it knows which file to get the contents from, and returns an IO String—an I/O action that holds contents of the file as its result. This function is pretty much like getContents. The only difference is that getContents will automatically read from the standard input (that is, from the terminal), whereas hGetContents takes a file handle that tells it which file to read from. In all other respects, they work the same.
Just like