Edit 2: I've written two, better articles about IO in Haskell, see the my haskell-study-plan and my book

Edit: After writing this post I turned to reddit for advice on how to make this post better, and even after a complete re-write It still felt lacking. After asking about it at #haskell, merijn linked to this article which in my opinion explains this subject much better than mine. So you might want to read it before this post or even instead of.

Haskell is a purely functional language, but what does being "pure" mean?

The pure in purely functional means that Haskell enforces the separation between evaluating an expression and executing an expression.

An expression can be thought about simply as a value. In order to produce a value, an expression needs to be evaluated (or calculated).

While evaluated, expressions are stateless and cannot do anything other than compute a value and return it, and will also return the same value. Always. This also means it can't mutate a variable, read from file or write to standard output. We say that evaluating expressions is pure.

All expressions in Haskell are pure, which mean that we can evaluate any expression. But some expressions may need to do more than just being evaluated in order to do something useful. Some expressions needs to do other things in order to produce a meaningful value, this kind of expressions can be executed. Let's call this kind of expressions IO actions.

Producing value just by evaluating expressions is simpler and more straightforward in Haskell, while executing expressions requires more attention and can only happen at certain places. Most expressions in Haskell do not need to be executed in order to produce a meaningful value.

Haskell enforces this separation between pure expressions and IO actions using it's type system at compile time.

We can differentiate between pure expressions and IO actions by looking at the type of an expression. An IO action's type signature is slightly different from a pure function or value.

For example:

val1 :: Int

val2 :: IO Int

func1 :: Int -> Int

func2 :: Int -> IO Int

val1 and func1 are pure - they don't need to be executed to produce an Int, only evaluated. val2 and func2 are IO actions - in order to produce an Int they need to be executed. We can tell because they return an IO _ type.

Let's say we want to write a program that reads an Int from a user, and multiply it by a multiplier. Here is a naive attempt where we treat an IO Int as equal to Int, which means that when evaluating IO Int we also execute it and produce an Int:

readIntFromUser :: IO Int
readIntFromUser = ...

mul :: Int -> Int -> Int
mul = (*)

userInt :: IO Int
userInt = readIntFromUser

multiplier :: Int
multiplier = 3

main = print (mul multiplier userInt)

Well, this might be a little bit problematic, because now we lost the ability to separate evaluation from execution. Also, since the type of mul is Int -> Int -> Int, the type of mul multiplier userInt is also an Int, so now we don't know if we are going to get the same value every time or not. So we can't replace the implementation of mul by something that, for example, sums a multiplier of Ints.

Pure expressions allow us to do "programming algebra" like this without unexpected side-effects.

So, we will not treat an IO Int as an Int and thus retain the separation of evaluation and execution! (Means this is not valid Haskell code.)

Okay, so, how do we multiple a user read Int by multiplier?

Hmm. Perhaps we can use a function that can execute an IO action, thus converting IO Int to Int? Let's call a function like that execute.

But now, it is also entirely possible to rewrite multiplier like this:

multiplier :: Int
multiplier = if execute userInt == 3 then 3 else 3

multiplier, in this case, still equals 3 but now it also reads user input, so it is not pure even though it's type is Int and will return 3 every time!

So, converting an IO a type to a by executing it whenever we want is rejected. (Not valid Haskell either.)

Let's look at this from a different perspective, what if we could take the function we want to apply to the IO Int value, apply it to the Int that would be calculated when executed, and return a new IO Int with the new value after applying the function? That way we can still keep the separation of pure expressions and IO actions with types!

And indeed, we can. Using fmap.

fmap :: (a -> b) -> IO a -> IO b
fmap = ...

main :: IO ()
main = print (fmap (mul multiplier) userInt) -- ==> error: print cannot take IO Int.

Yes! We produced an IO action that will take a user input and will multiply it by multiplier! But now we have a different problem, print doesn't want to print our IO Int, maybe it wants an Int? Let's try the same trick we just discovered on print as well.

main :: IO ()
main = fmap (print (fmap (mul multiplier) userInt)) -- ==> error: main type mismatch between IO () and IO (IO ())

Hmm, we were able to apply print to our computation, but print returns an IO (), so now after using fmap our main function has a type of IO (IO ()) and not IO ().

As we said, IO actions are just like regular values that can be evaluated (in the same way a function can be evaluated). They can be returned from a function or stored in a data structure. It is just that if we want to produce a value from them, we have to execute them.

Another analogy that might be helpful is to think about IO actions as "plans" to produce a value. For example, an IO a action is a plan to produce a value of type a. The plan itself is a regular, first class value. But in order to produce the a value of the plan, we need to execute it.

Sometimes we want to return an IO action to be executed later, but in this case, we just want to execute this IO action. So, what if we could join two IO together to one IO so we can think of them as one plan to execute a value?

Apparently, we can. Using join.

join :: IO (IO a) -> IO a
join = ...

main :: IO ()
main = join (fmap (print (fmap (mul multiplier) userInt)))

Great! Now everything typechecks! But it looks like a lot of work. Can't we make it more concise?

bind :: (a -> IO b) -> IO a -> IO b
bind f x = join (fmap f x)

main :: IO ()
main = bind (print . mul multiplier) userInt

Now, what if, for example, We want to test this function by supplying a value (like -3), we can use pure to create a "plan" to produce a specific Int. By the way, this IO action will not do anything beyond returning a value when executed, since we gave it a value to produce, it doesn't need to do anything in order to produce it.

userInt :: IO Int
userInt = pure (-3)

main :: IO ()
main = bind (print . mul multiplier) userInt

Okay, But we still haven't figured out when to execute an IO action and how.

Let's decide that we execute main, by executing the IO actions that compose it, be it one IO action or more that are composed with bind, and in the order they are sequenced (since you can't print without knowing what is the value the user entered). That way, we can still execute IO actions as much as we like, but we will also be able to say "anything beyond this is pure".

A short summary

Let's stop for a moment and go over how we wanted to create and enforce the separation of pure code and IO actions and what we discovered in the process.

  • We decided to use types to differentiate between expressions that can be executed and expressions that cannot. Expressions that can be executed has IO in their type.
  • We decided that we don't want to treat IO a as a - we don't want execution to happen on evaluation.
  • We decided that we don't want to be able to convert IO a to a (which means to execute it anywhere we want), and so once we are in IO context, we can't "escape" it.
  • We decided that we can convert a to IO a using pure, supplying a value to return on execution. (you can also use a function called return that does the same thing. In GHC versions older than 7.10 and other Haskell implementations, pure, which can be found in the module Control.Applicative, is not exported by default, but return is.)
  • We saw that we can use pure functions like (a -> b) to change IO a to IO b, using fmap.
  • We saw that we can chain IO actions using bind. (Which in Haskell is known as =<<. Actually, the function >>= is more commonly used which is just =<< with the arguments flipped. With >>= it looks like we are chaining IO actions from left to right)
  • We decided that main will be executed by executing the IO actions that compose it.

Now, let's write the program we wanted again using what we have learned, and let's make the use of >>= a little bit more explicit by using lambda expressions instead of just currying.

readIntFromUser :: IO Int
readIntFromUser = getLine >>= (\line -> pure (read x)) -- which is the same as: getLine >>= pure . read

userInt = IO Int
userInt = readIntFromUser

multiplier :: Int
multiplier = 3

mul :: Int -> Int -> Int
mul = (*)

main :: IO ()
main = userInt >>= (\input -> pure (mul multiplier input)) >>= (\result -> print result)

Now, let's say we want to first print the user's input and then the result, we could change main to:

main :: IO ()
main = userInt >>= (\input -> pure (mul multiplier input) >>= (\result -> print input >>= (\_ -> print result)))

But this becomes a little clumsy and not very readable. Fortunately, Haskell has special syntax known as do notation, let's rewrite main using it.

main :: IO ()
main = do
  input  <- userInt
  result <- pure (mul multiplier input)
  _      <- print input
  print result

This looks better. Note that input <- userInput is analogous to userInput >>= \input -> .... Also, We can do even better than that syntax-wise! We can replace _ <- ... with just ... and we can replace result <- pure ... with let result = .... Let's see what this looks like!

main :: IO ()
main = do
  input <- userInt
  let result = mul multiplier input
  print input
  print result

Now we can write IO code to be executed, in a concise way, while still retaining the ability to separate evaluation from execution.

There are still more things you can do with IO that are more concise, but with knowing what you know now you have the full power IO and you can do whatever you want with it, including building your own higher order functions to manipulate it.