Learn You a Haskell for Great Good! - Miran Lipovaca [76]
We’ve made the I/O part of our program as short as possible. Because our program is supposed to print something based on some input, we can implement it by reading the input contents, running a function on them, and then printing out what that function gives back.
The shortLinesOnly function takes a string, like "short\nlooooooong\nbort". In this example, that string has three lines: two of them are short, and the middle one is long. It applies the lines function to that string, which converts it to ["short", "looooooong", "bort"]. That list of strings is then filtered so that only those lines that are shorter than 10 characters remain in the list, producing ["short", "bort"]. Finally, unlines joins that list into a single newline-delimited string, giving "short\nbort".
Let’s give it a go. Save the following text as shortlines.txt.
i'm short
so am i
i am a loooooooooong line!!!
yeah i'm long so what hahahaha!!!!!!
short line
loooooooooooooooooooooooooooong
short
And now we’ll compile our program, which we saved as shortlinesonly.hs:
$ ghc --make shortlinesonly
[1 of 1] Compiling Main ( shortlinesonly.hs, shortlinesonly.o )
Linking shortlinesonly ...
To test it, we’re going to redirect the contents of shortlines.txt into our program, as follows:
$ ./shortlinesonly < shortlines.txt
i'm short
so am i
short
You can see that only the short lines were printed to the terminal.
Transforming Input
The pattern of getting some string from the input, transforming it with a function, and outputting the result is so common that there is a function that makes that job even easier, called interact. interact takes a function of type String -> String as a parameter and returns an I/O action that will take some input, run that function on it, and then print out the function’s result. Let’s modify our program to use interact:
main = interact shortLinesOnly
shortLinesOnly :: String -> String
shortLinesOnly = unlines . filter (\line -> length line < 10) . lines
We can use this program either by redirecting a file into it or by running it and then giving it input from the keyboard, line by line. Its output is the same in both cases, but when we’re doing input via the keyboard, the output is interspersed with what we typed in, just as when we manually typed in our input to our capslocker program.
Let’s make a program that continuously reads a line and then outputs whether or not that line is a palindrome. We could just use getLine to read a line, tell the user if it’s a palindrome, and then run main all over again. But it’s simpler if we use interact. When using interact, think about what you need to do to transform some input into the desired output. In our case, we want to replace each line of the input with either "palindrome" or "not a palindrome".
respondPalindromes :: String -> String
respondPalindromes =
unlines .
map (\xs -> if isPal xs then "palindrome" else "not a palindrome") .
lines
isPal :: String -> Bool
isPal xs = xs == reverse xs
This program is pretty straightforward. First, it turns a string like this:
"elephant\nABCBA\nwhatever"
into an array like this:
["elephant", "ABCBA", "whatever"]
Then it maps the lambda over it, giving the results:
["not a palindrome", "palindrome", "not a palindrome"]
Next, unlines joins that list into a single, newline-delimited string. Now we just make a main I/O action:
main = interact respondPalindromes
Let’s test it:
$ ./palindromes
hehe
not a palindrome
ABCBA
palindrome
cookie
not a palindrome
Even though we created a program that transforms one big string of input into another, it acts as if we made a program that does it line by line. That’s because Haskell is lazy, and it wants to print the first line of the result string, but it can’t because it doesn’t have the first line of the input yet. So as soon as we give it the first line of input, it prints the first line of the output. We get out of the program by issuing an end-of-line character.
We can also use this program by just redirecting a file into it.