My Newbie Experience With Haskell’s IO Monad

So I’ve been on the Haskell train for about two and a half months now, and wrote 1804 lines of Haskell code according to cloc. There are oodles of articles out there extolling the virtues of Haskell, so I want to limit this post to just one thing: the I/O Monad (with a discussion on bind operators, do-notation, and the return function) and what I’ve learned about them through many lines of trial and error. As you bump into do-notation more and more, you’ll have to eventually learn what it really means, and how the return and bind ((>>) and (>>=)) functions play a big role in any monadic operation. This post is meant for Haskell newbies, like myself two and a half months ago.

DISCLAIMER: Before I start, I must warn you: I don’t have a “true” understanding of Monads, even after a couple thousand lines! I’ve greatly simplified everything down so that even an ultra-beginner-newbie would understand what I’ve written here. Some of my explanations are probably not “correct,” but hey, there’s no wrong in trying, right? Besides, it’s probably better to understand things imperfectly, like a child, and then slowly refine things over time. If you insist on at least a preview of the true understanding behind Monads, go to this page first (but if you really are a Haskell newbie I seriously doubt you’d benefit from it).

In a simple “Hello World” program, you have something like this:

main :: IO ()
main = putStrLn "Hello world!"

The type signature above looked very scary to me for a long time. Not any more. The type “IO ()”, in plain English, means: “This is a function that does some I/O stuff at some point, and in the end, does not return any value to the calling function.” The () (aka “unit”) is Haskell’s rough equivalent of a NULL value in C.

An equivalent (as far as types are concerned) way to write the above function is:

main :: IO ()
main = do   {
            ; putStrLn "Hello world!"
            ; return ()
            }

or (since do-notation in the above example is a little clunky, even without the braces and semicolons)

main :: IO ()
main = putStrLn "Hello world!" >> return ()

But, the putStrLn function already results in a “IO ()” by itself:

putStrLn :: String -> IO ()

so doing

main :: IO ()
main = do   {
            ; putStrLn "Hello world!"
            ; return ()
            }

is redundant. It’s like doing this:

main :: IO ()
main = do   {
            ; putStrLn "Hello world!"
            ; return ()
            ; return ()
            ; return ()
            ; return ()
            ; return ()
            }

Which is still valid, but obviously redundant. If the above code’s validity surprised you, then you are in good shape. (See the discussion about the return function below.) Anyway, sorry for the detour. Let’s focus on this example:

main :: IO ()
main = putStrLn "Hello world!" >> return ()

The (>>) operator just means “I don’t care what is on the left side; just compute what’s on the right hand side.” In this case, we explicitly force the computation of “return ()”, after computing the “putStrLn” portion. But the use of a “return ()” explicitly is often unnecessary and wordy in actual practice, at least with the IO monad, because functions like putStrLn (as I stated above), and putStr already give back a “IO ()”.

Here is a funny example: you could do something (meaningless) like this:

main :: IO (Int)
main = putStrLn "Hello world!" >> return 123

And the code will compile and work just as well as the preceding example. The only conceptual flaw is that since main is the only function, no other function actually uses the main function’s (return 123) computation, so it becomes absolutely meaningless.

Let’s look at something a little more complicated.

import IO

main :: IO ()
main = do   {
            ; hSetBuffering stdout NoBuffering
            ; hSetBuffering stdin NoBuffering
            ; putStr "Press a number key: "
            ; c <- getChar
            ; putStrLn ""
            ; natureOfC <- isThisADigit c
            ; if natureOfC == True
                    then putStrLn "You pressed a number key."
                    else putStrLn "You pressed something other than a number key."
            }

isThisADigit :: Char -> IO Bool
isThisADigit c = if elem c ['0'..'9']
                    then return (True)
                    else do {
                            ; putStrLn "Error: non-number character detected."
                            ; return (False)
                            }

I want you to understand the “isThisADigit” function. Here, the “IO Bool” type tells us that this function, at some point, does some IO, before resulting in a “IO Bool” value. The caller must “extract” the pure Bool value out of this “tainted” IO Bool type, with the left arrow <-. In the example above, the variable “natureOfC” captures this extracted Bool value.

The key about the IO Monad (like all other monads) is that they sort of act as a warning sign for all of the rest of your program’s pure code. Any function with a monad in its type signature is a big red flag that says, “Hey! I’m a dangerous function that results in lots of side effects/changed state! Be careful when calling me!” This is great because now we can easily tell, based on a function’s type signature alone, whether it’s pure or not. Hiding impure code in Haskell is thus impossible to do, as long as you explicitly write down your functions’ type signatures.

Here’s another example (just to illustrate the separation of pure vs. impure code in a real example):

import IO

main :: IO ()
main = do   {
            ; hSetBuffering stdout NoBuffering
            ; hSetBuffering stdin NoBuffering
            ; putStr "Press a number key: "
            ; c <- getChar
            ; putStrLn ""
            ; natureOfC <- isThisADigit c
            ; if natureOfC == True
                    then putStrLn "You pressed a number key."
                    else putStrLn "You pressed something other than a number key."
            ; putStrLn $ "You get " ++ (show $ handleBool natureOfC) ++ " points for your answer."
            }

isThisADigit :: Char -> IO Bool
isThisADigit c = if elem c ['0'..'9']
                    then return (True)
                    else do {
                            ; putStrLn "Error: non-number character detected."
                            ; return (False)
                            }

handleBool :: Bool -> Int
handleBool b = if b == True
                    then 100
                    else 0

The new handleBool function is the only pure function above, and its job is to convert a bool value into a single Int value, of either 100 or 0. The fact that the line “natureOfC <- isThisADigit c” extracts the pure value out of the impure “IO Bool” allows us to use the handleBool function with it.

It may help to write “IO (Bool)” instead of “IO Bool” if you want to get the visual image of a pure value being “wrapped” inside the IO monad.

Here’s another example (a rather contrived, un-idiomatic Haskell example), using lots of if/then/else statements just to illustrate the notion that the function return used in the context of a monad does NOT mean the same thing as “return” in C.

import IO
import qualified System.Exit as SE

main :: IO ()
main = do   {
            ; hSetBuffering stdout NoBuffering
            ; hSetBuffering stdin NoBuffering
            ; putStr "Press a lowercase character key that is between 'h' and 'o' (inclusive): "
            ; c <- getChar
            ; putStrLn ""
            ; isLower <- isThisLowercase c
            ; if isLower == True
                    then return ()
                    else SE.exitWith $ SE.ExitFailure 1
            ; isLessThanH <- isThisLessThanH c
            ; if (not isLessThanH)
                    then return ()
                    else SE.exitWith $ SE.ExitFailure 2
            ; isGreaterThanO <- isThisGreaterThanO c
            ; if (not isGreaterThanO)
                    then return ()
                    else SE.exitWith $ SE.ExitFailure 3
            ; putStrLn $ "You pressed `" ++ [c] ++ "'! Congrats!"
            }

isThisLowercase :: Char -> IO Bool
isThisLowercase c = if elem c ['a'..'z']
                        then return True
                        else do {
                                ; putStrLn $ "Error: char `" ++ [c] ++ "' not lowercase"
                                ; return False
                                }

isThisLessThanH :: Char -> IO Bool
isThisLessThanH c = if elem c ['a'..'g']
                        then do {
                                ; putStrLn "Error: char less than `h'"
                                ; return True
                                }
                        else return False

isThisGreaterThanO :: Char -> IO Bool
isThisGreaterThanO c = if elem c ['p'..'z']
                            then do {
                                    ; putStrLn "Error: char greater than `o'"
                                    ; return True
                                    }
                            else return False

I want you to focus on the series of if/then/else statements in the main function. Here, we can see that we test 3 properties for the user-given character, c: if it is lowercase, if it is less than ‘h’, and if it is greater than ‘o’. If any three of these conditions are met, we immediately exit the program with an error, using the System.Exit module. Otherwise, we just “return ()” for this stage of the overall do-notation computation. Here’s some pseudocode (with the exception of the (>>) operator, which retains its meaning) to explain:

TEST WAS PASSED, so:
return () >> carry on with the next computation that follows…

Maybe that made it a little clearer. The big idea with monadic code (and do-notation) is this: every step of the computation needs to be monadic! So in the main function above, if we pass a test, we have to sort of “glue” the remaining function together by “injecting” a “return ()” if we pass the test. Or, more accurately, we “inject” the unit value “()” into the IO monad with the “return” function. This allows us to carry on with the rest of the do-notation.

Why do we have to have monadic values at each step of a monadic operation (do-notation)? Well, it’s because do-notation is syntactic sugar for a series of computations requiring the use of the bind (>> or >>=) operators. And the bind operators’ type signatures are mandated as:

(>>=) :: m a -> (a -> m b) -> m b
(>>)  :: m a -> m b -> m b

So in the case of the IO monad (hooray Haskell typeclasses!), the bind operators mean:

(>>=) :: IO something -> (something -> IO something_else) -> IO something_else
(>>)  :: IO something -> IO something_else -> IO something_else

Notice how both bind operators requires that its left-side value (they are used as infix operators) be a monadic value, “m a” or in our example, IO something. So that’s why we are required to put in the “return ()” in the if/then/else statements in our last big example: the do-notation that we use in there requires that every step of the do-notation results in a monadic value, be it () or whatever. The return function allows us to meet this requirement: it injects a pure value into a monad:

return :: a -> m a

If the meaning of return still eludes you, you can think of it as a train conductor: a “return (140)” means “I, return, as train conductor of the Monad typeclass train, now allow you, pure value 140, to enter this train. The only way for you to get off the Monad train is with the (<-) operator, understood?”

Here is another example to illustrate this last point:

main :: IO ()
main = do   {
            ; pureNum <- boardTrain (140)
            ; putStrLn $ show pureNum
            }

boardTrain = return

So now you know what the return function is all about: injecting pure values into the Monad typeclass, so that you can use them as part of (larger) monadic operations, be it a big do-notation function or a simple monadic function that figures out a pure value, like the isThisLowercase function.

Here are some more explanations of the bind operator (using the IO monad) in plain English:

(>>=) :: IO something -> (something -> IO something_else) -> IO something_else

Plain English: Take a (IO) monadic value, and a function that takes a pure version of this value and converts it into some new monadic value, and give back this new monadic value. The (>>=) operator is thus useful if you want to “pass along” the results of one monadic computation to another monadic computation.

(>>)  :: IO something -> IO something_else -> IO something_else

Plain English: Take a (IO) monadic value, and another monadic value, and give back this latter monadic value. I.e., compute the first value, but we don’t care what the result of this first monadic operation is; just continue on with the second monadic value (IO something_else) after we’re done with the first one (IO something). The (>>) operator is thus just like (>>=), except that we do not pass along any values from one monadic operation to the next.

The reason why the (>>) and (>>=) operators are called “bind” operators might have dawned upon you now: they allow you to “chain” together multiple monadic computations together! This is why some Haskellers like to think of Monads as just another form of function composition, but for impure functions. Indeed, do-notation is just syntactic sugar for these two bind operators that allow you to easily “glue” together small, impure functions into a larger *sequence*.

There is also a “flipped” or “reverse” bind operator, (=<<), but with the arguments switched around. It’s useful if, for syntactic reasons you want to pass along a monadic value in the opposite “direction.” Usually, you use it when you want to visualize one monadic operation as an “argument” for another, to aid reading from left to right. For example:

teamWork :: IO ()
teamWork = putStrLn =<< getLine

Here, the contents of getLine act as an argument to the function putStrLn. Here’s another example:

C version:
        x = get_x_value();
        printf("%f\n", (round(abs(sqrt(x)))));

Haskell version:
teamWork :: IO ()
teamWork = putStrLn =<< round =<< abs =<< sqrt =<< get_x_value

See how the reverse bind operator allows us to mimic what C looks like? The reverse bind operator lets us write things in a more natural way, mimicking the syntax of pure function composition, such as “e . d . c . b $ a”, where the order of computation reads right-to-left. Personally, I rarely use the (=<<) operator because I use do-notation whenever I need to do some serious monadic computations — but, it's there when you need it.

Here's an example using a bunch of bind operators to give you their feel.

import IO

main :: IO ()
main     = putStr "Tell me your name: "
        >> hFlush stdout
        >> getLine
        >>= greet1
        >> putStr "Tell me your age: "
        >> hFlush stdout
        >> (greet2 =<< getLine)
        >> putStrLn "Bye!"
        where   greet1 str = putStrLn ("Hello, " ++ decorate str ++ "!")
                greet2 str = putStrLn ("You are " ++ decorate str ++ " years old!")

decorate :: String -> String
decorate xs = "-=xX" ++ xs ++ "Xx=-"

Notice how avoiding do-notation obviates the need to use the left arrow (<-) operator for getLine.

Let us revisit do-notation one more time:

import IO

main :: IO ()
main = do   {
            ; putStr "Tell me your name: "
            ; hFlush stdout
            ; name <- getLine
            ; putStrLn ("Hello, " ++ decorate name ++ "!")
            }

decorate :: String -> String
decorate xs = "-=xX" ++ xs ++ "Xx=-"

The point I want to make in this example is, believe it or not, the main function’s type signature. After everything I told you about how bind-operator-this and do-notation-that all work together to chain together sequences of monadic operations, I want you to reflect back on main‘s type signature: after all the fancy work that it does in the example above, main‘s type signature is just “IO ()”! The type signature of main is the type signature of the last monadic operation: in this case, the type signature for putStrLn.

Here’s a contrived example just for kicks, to encourage you to avoid do-notation for simple functions:

everyManForHimself :: SomeMonad (func4's return type)
everyManForHimself = func1 >> func2 >> func3 >> func4

The function above shows how the weak bind operator (>>) merely sequences independent monadic operations together, who all refuse to interact with each other. The component functions func1, func2, etc. may or may not have meaningful return types; i.e., func1 may be a “return ()” type, or a “return (Bool)” or something else — but the point is that the (>>) operator forcefully discards whatever is on the left and just moves on to the next operation.

Hopefully, this post shed some light into some non-intuitive examples on the IO monad. I hope things like IO Char, IO Int, IO Bool, IO String, and other types feel more natural now to you.

Reference: The “Gentle Introduction to Haskell, Version 98” has a good discussion of do-notation here.

UPDATE April 7, 2011: Apparently, the use of if/then (some monadic operation)/else (no monadic operation) pattern is so common that there is a shorter way. Simply import the when function from the Control.Monad module. So instead of

if (blah blah)
    then putStrLn "OK"
    else return ()

you can just do

when (blah blah) $ putStrLn "OK"

Moral of the story: if you ever feel like you are repeating yourself too much, look into the standard libraries (like Control.Monad).

UPDATE August 4, 2011: Edited discussion of the reverse bind operator.