Learn You a Haskell for Great Good! - Miran Lipovaca [75]
import Control.Monad
import Data.Char
main = forever $ do
l <- getLine
putStrLn $ map toUpper l
Save this program as capslocker.hs and compile it.
Instead of inputting lines via the keyboard, we’ll have haiku.txt be the input by redirecting it into our program. To do that, we add a < character after our program name and then specify the file that we want to act as the input. Check it out:
$ ghc --make capslocker
[1 of 1] Compiling Main ( capslocker.hs, capslocker.o )
Linking capslocker ...
$ ./capslocker < haiku.txt
I'M A LIL' TEAPOT
WHAT'S WITH THAT AIRPLANE FOOD, HUH?
IT'S SO SMALL, TASTELESS
capslocker What we’ve done is pretty much equivalent to running capslocker, typing our haiku at the terminal, and then issuing an end-of-file character (usually done by pressing ctrl-D). It’s like running capslocker and saying, “Wait, don’t read from the keyboard. Take the contents of this file instead!” Getting Strings from Input Streams In our capslocker.hs example, we used forever to read the input line by line and then print it back in uppercase. If we opt to use getContents, it takes care of the I/O details for us, such as when to read input and how much of that input to read. Because our program is about taking some input and transforming it into some output, we can make it shorter by using getContents: import Data.Char main = do contents <- getContents putStr $ map toUpper contents We run the getContents I/O action and name the string it produces contents. Then we map toUpper over that string and print that result to the terminal. Keep in mind that because strings are basically lists, which are lazy, and getContents is I/O lazy; it won’t try to read all of the content at once and store that into memory before printing out the caps-locked version. Rather, it will print out the caps-locked version as it reads, because it will read a line from the input only when it must. Let’s test it: $ ./capslocker < haiku.txt I'M A LIL' TEAPOT WHAT'S WITH THAT AIRPLANE FOOD, HUH? IT'S SO SMALL, TASTELESS So, it works. What if we just run capslocker and try to type in the lines ourselves? (To exit the program, just press ctrl-D.) $ ./capslocker hey ho HEY HO lets go LETS GO Pretty nice! As you can see, it prints our caps-locked input line by line. When the result of getContents is bound to contents, it’s not represented in memory as a real string, but more like a promise that the string will be produced eventually. When we map toUpper over contents, that’s also a promise to map that function over the eventual contents. Finally, when putStr happens, it says to the previous promise, “Hey, I need a caps-locked line!” It doesn’t have any lines yet, so it says to contents, “How about getting a line from the terminal?” And that’s when getContents actually reads from the terminal and gives a line to the code that asked it to produce something tangible. That code then maps toUpper over that line and gives it to putStr, which prints the line. And then putStr says, “Hey, I need the next line—come on!” This repeats until there’s no more input, which is signified by an end-of-file character. Now let’s make a program that takes some input and prints out only those lines that are shorter than 10 characters: main = do contents <- getContents putStr (shortLinesOnly contents) shortLinesOnly :: String -> String shortLinesOnly = unlines . filter (\line
Let’s take a look at an I/O action that makes processing input streams easier by allowing us to treat them as normal strings: getContents. getContents reads everything from the standard input until it encounters an end-of-file character. Its type is getContents :: IO String. What’s cool about getContents is that it does lazy I/O. This means that when we do foo <- getContents, getContents doesn’t read all of the input at once, store it in memory, and then bind it to foo. No, getContents is lazy! It will say, “Yeah yeah, I’ll read the input from the terminal later as we go along, when you really need it!”