September 13, 2026

Haskell for Hobos -- some Haskell IO

There are lots of interesting and important things to say about IO in Haskell, but I'm not going to say them here. This will be sort of a "monkey see, monkey do" sampler of some useful things that will help you write real programs.

You have seen this:

main = putStrLn "hello world"

Often you have important results in some variable. You can use either of these:

main = putStrLn $ show thing
main = print thing
What if you want to print several lines:
main = do
	putStrLn "first line"
	putStrLn "second line"
Don't get the idea that "do" syntax is only for IO. There is deep magic involving monads that we will discuss elsewhere. Do notation has other interesting uses in entirely different contexts.

Suppose you want to read a line from input and do something with it:

mangle x = ">>> " ++ x ++ " <<<"

main = do
    print "Please type something"
    xyz <- getLine
    let newxyz = mangle xyz
    print newxyz
Notice here that we call getLine, but need to use the <- operator to get the value. We can't just say xyz = getLine, because getLine has the type of an IO action and it contains the string. The <- operator extracts it. This is part of the do notation deep magic.

In the same vein, look at the next line. We can't just use "newxyz = mangle xyz" like we would outside of the "do" world. We have to prefix such statements of this sort with "let". More of the unique "do" business we will explain elsewhere. This is "monkey see, monkey do" for now.

I don't know about you, but I hardly ever write programs that prompt the user for input and then read from the terminal. I read and write files. So let's see how we do that.

Filters

Haskell has a cute function for writing filters. If you want to write a Haskell program that reads from stdin and writes to stdout, this might be the thing for you. It effectively reads the entire input file as one string.
main = interact filter_func
Remember, a string is a list of Char types. So we can do this:
main = interact $ show . length
Then if we type "count <file" we will get the number of characters in the file. If we want to count lines, we can do this:
main = interact $ show . length . lines
If you want a newline at the end of that number, try:
lcount x = val ++ "\n"
    where val = (show . length . lines) x

main = interact lcount
If you just want to ignore the input and output a silly message, try this:
bogus _ = "insert coin\n"
main = interact bogus
If you want that all on one line, you can use a lambda function:
main = interact (\_ -> "insert coin\n")

Something useful

Suppose we want to reverse all the lines in a file. Without thinking too much, I tried this first:
main = interact reverse
It works in a way, but it reverses the entire file.
The following does what we want:
main = interact (unlines . map reverse . lines)
The "lines" function gives us a list of lists. Remember, a string is just a list of Chars. It removes the newlines at the end of each line, which is handy because we don't want to include the newlines in the reversing. You might think we could then use "concatmap", and we could if we didn't want the newlines put back in properly. The unlines function is just the inverse of lines, it puts the newlines back in, along with joining the list of lists back into a flat list.

I'll note in passing that Haskell offers a function "intercalate" that joins a list of lists, inserting any string we want between them to join them together. It is almost the same as "unlines" once we specify the joining string to be a newline, but it doesn't add the final newline on the last string.

All of this is more like exploring a menu at a restaurant than it is learning new Haskell concepts, so let us move on.

Files

Reading a file is nice and easy:
main = do
	stuff = readFile "/etc/passwd"
I am curious and try this:
num = length stuff

main = do
    stuff <- readFile "/ect/passwd"
    print num
I get an error. Haskell scolds me that "stuff" is not in scope when I try to use it as an argument to length.

That aside, you may be balking at the idea of reading the entire file into the "stuff" variable. You have to rely on Haskell laziness to handle this efficiently. Nothing actually happens until we try to access "stuff" and then Haskell will do something sensible about doling out what is needed bit by bit.

What happens if we try to read a non-existant file? Haskell blows up with a long and frightening backtrace, that is what happens. So how to we handle this is a nice way?

This deserves a page of its own. In short, Haskell has an exception handling mechanism that uses "throw" and "try". You wrap the file open in try, which returns and Either type (which we have not yet talked about). You get a Left reponse on an error and a Right response on success. This deserves more detailed explanation elsewhere.
However, the following is the general scheme, just so you know. Writing code that handles real world errors is always more work.

import Control.Exception (try)
import System.IO.Error (IOError)

rFile :: FilePath -> IO (Either IOError String)
rFile path = try ( readFile path )

main = do
    result <- rFile "/etc/passwd"
    case result of
        Left ex  -> putStrLn $ "Failed to open file: " ++ show ex
        Right stuff -> putStrLn "File opened successfully!"
Taking this a bit farther:
main = do
    result <- rFile "/etc/passwd"
    case result of
        Left ex  -> putStrLn $ "Failed to open file: " ++ show ex
        Right stuff -> do
            putStrLn "File opened successfully!"
            putStrLn $ " " ++ (show $ length stuff) ++ " lines in file"
To get the "Right" case to have more than a single line of stuff, we drag in "do" notation. It is important to know that "case" is an expression (being a function like all things in Haskell) and must return a value, which is this case is IO () due to the PutStrLn inside. But we are wandering too far off the main topic for sure.


Have any comments? Questions? Drop me a line!

Tom's software pages / tom@mmto.org