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 Int
s.
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
asa
- we don't want execution to happen on evaluation. - We decided that we don't want to be able to convert
IO a
toa
(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
toIO a
usingpure
, supplying a value to return on execution. (you can also use a function calledreturn
that does the same thing. In GHC versions older than 7.10 and other Haskell implementations,pure
, which can be found in the moduleControl.Applicative
, is not exported by default, butreturn
is.) - We saw that we can use pure functions like
(a -> b)
to changeIO a
toIO b
, usingfmap
. - 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.