Interactive IO
Today we'll continue our study of IO by looking at an interactive IO program. In this kind of program, the user will enter commands continuously on the command line to interact with our program. The fun part is that we'll find a use for a lesser-known library function called, well, interact
!
Imagine you're writing a command line program where you want the user to keep entering input lines, and you do some kind of processing for each line. The most simple example would be an echo program, where we simply repeat the user's input back out to them:
>> Hello
Hello
>> Goodbye
Goodbye
A naive approach to writing this in Haskell would use recursion like so:
main :: IO ()
main = go
where
go = do
input <- getLine
putStrLn input
go
However, there's no terminal condition on this loop. It keeps expecting to read a new line. Our only way to end the program is with "ctrl+C". Typically, the cleaner way to end a program is to use the input "ctrl+D" instead, which is the "end of file" character. However, this example will not end elegantly if we do that:
>> Hello
Hello
>> Goodbye
Goodbye
>> (ctrl+D)
<stdin>: hGetLine: end of file
What's happening here is that getLine
will throw this error when it reads the "end of file" character. In order to fix this, we can use these helper functions.
hIsEOF :: Handle -> IO Bool
-- Specialized to stdin
isEOF :: IO Bool
These give us a boolean that indicates whether we have reached the "end of file" as our input. The first works for any file handle and the second tells us about the stdin
handle. If it returns false, then we are safe to proceed with getLine
. So here's how we would rewrite our program:
main :: IO ()
main = go
where
go = do
ended <- isEOF
if ended
then return ()
else do
input <- getLine
putStrLn input
go
Now we won't get that error message when we enter "ctrl+D".
But for these specific problems, there's another tool we can turn to, and this is the "interact" function:
interact :: (String -> String) -> IO ()
The function we supply simply takes an input string and determines what string should be output as a result. It handles all the messiness of looping for us. So we could write our echo program very simply like so:
main :: IO ()
main = interact id
...
>> Hello
Hello
>> Goodbye
Goodbye
>> Ctrl+D
Or if we're a tiny bit more ambitious, we can capitalize each of the user's entries:
main :: IO ()
main = interact (map toUpper)
...
>> Hello
HELLO
>> Goodbye
GOODBYE
>> Ctrl+D
The function is a little tricky though, because the String -> String
function is actually about taking the whole input string and returning the whole output string. The fact that it works line-by-line with simple functions is an interesting consequence of Haskell's laziness.
However, because the function is taking the whole input string, you can also write your function so that it breaks the input into lines and does a processing function on each line. Here's what that would look like:
processSingleLine :: String -> String
processSingleLine = map toUpper
processString :: String -> String
processString input = result
where
ls = lines input
result = unlines (map processSingleLine ls)
main :: IO ()
main = interact processString
For our uppercase and id examples, this works the same way. But this would be the only proper way to write our program if we wanted to, for example, parse a simple equation on each line and print the result:
processSimpleAddition :: String -> String
processSingleAddition input = case splitOn " " input of
[num1, _, num2] -> show (read num1 + read num2)
_ -> "Invalid input!"
processString :: String -> String
processString input = result
where
ls = lines input
result = unlines (map processSimpleAddition ls)
main :: IO ()
main = interact processString
...
>> 4 + 5
9
>> 3 + 2
5
>> Hello
Invalid input!
So hIsEOF
and interact
are just a couple more tools you can add to your arsenal to simplify some of these common types of programs. If you're enjoying these blog posts, make sure to subscribe to our monthly newsletter! This will keep you up to date with our newest posts AND give you access to our subscriber resources!