Irken Kitties

Recreating the Haskell List Part 6: The IO Monad

This is part 6 of a 6 part tutorial

The IO Monad

In pure functional programming, all functions are supposed to be referentially transparent, and that means that each time you call a function with the same arguments, it should give you the exact same result. When functions are referentially transparent, you have a lot less worries about whether or not it will always work correctly.

A mathematical function is never going to give you a different answer no matter how many times you give it the same argument. The reason for that is pretty much that it cannot get any values from anywhere other than what you passed it, so it can never be any different.

In an imperative programming language you could write a sin(x) function which was completely evil and called time(), getting a value from somewhere besides the x parameter. If the time in seconds was even, it would add 1 to the result it returns, and if not it wouldn’t.

This example is just plain evil, especially if every time you happen to test the sin() function it happened to be an odd time in seconds, until one important day a million astronauts burn to death in the depths of space because it was run on an even second. Silly example but that is the nature of many bugs in the imperative programming world.

All of these problems involve IO. If you say no functions can do any input or output to the OS, then the problem is solved, except you can also never interact with the program in any way.

The answer is to let some functions do IO, but do it inside a container called the IO Monad from which you aren’t supposed to be able to escape. The reason you aren’t able to escape, is because the data constructor for IO is hidden from use, by hiding it in the IO Module. This means the type signature for every function which does IO will be something like main :: IO ().

Any function which calls another function that does IO, getLine :: IO String, for example, must also return something wrapped in IO. It can’t deconstruct the return value from getLine into just a String using the IO data constructor and return that. It can pass the pure string to a pure function though, by using bind.

Here is an example of doing IO to get a number to pass to the pure function sin.

Doing some IO in the IO Monad

1
2
3
4
5
getSin :: IO ()
getSin = do
    valueStr <- getLine
    let result = sin (read valueStr :: Float)
    putStrLn $ "sin(" ++ valueStr ++ ") = " ++ (show result)

This looks like imperative code, telling you which order to do things and sharing the results of subsequent function calls. getLine returns a type IO String, remember this is like Container String from part 1.

The function putStrLn always returns IO (), read IO null, and since it is the last thing, that is returned from the entire function, as you would expect.

In reality, it is converted to this:

Explicitly using bind

1
2
3
4
5
getSin2 :: IO ()
getSin2 =
    getLine >>=
        (\valueStr -> let result = sin (read valueStr :: Float) in
            putStrLn $ "sin(" ++ valueStr ++ ") = " ++ (show result))

This is really one long expression, and not a recipe as it looks in do notation. Since haskell is lazy, it probably does not do any computation until it reaches the putStrLn function, which I think is strict (evaluates it as soon as it sees it).

When putStrLn evaluates its argument it finds valueStr and finds that it does’t have the value worked out yet. It sees that it comes passed in through the lambda and that forces it to call getLine, and the use enters their text. Then it evaluates the let statement to find the result, has a complete string, prints it out, and putStrln returns IO () from the lambda, and according to the definition of bind, also returns IO () from the bind expression, and then the function getSin2 itself.