I always find it confusing when people claim that the IO monad somehow makes an impure function pure. Frankly, I think that argument does a confusing disservice to people who are trying to learn functional programming (FP).
A thought exercise
For instance, here’s a little thought exercise:
Imagine a function you know nothing about, but when you call it one time with the string
"Hello"
it returnsOption["Fred"]
, and then you call it a second time with"Hello"
and it returnsOption["Mary"]
. Would you say that this function is pure or referentially transparent?
No, of course not. It gives you a different result each time you pass in the same parameter.
Now, just replace Option
with IO
in that thought exercise. Is the function somehow pure?
Quote from Learn You a Haskell for Great Good
I thought of this again today when I read this quote in the excellent book, Learn You a Haskell for Great Good:
“You can think of an I/O action as a box with little feet that will go out into the real world and do something there (like write some graffiti on a wall) and maybe bring back some data. Once it has fetched that data for you, the only way to open the box and get the data inside it is to use the
<-
construct. And if we’re taking data out of an I/O action, we can only take it out when we’re inside another I/O action. This is how Haskell manages to neatly separate the pure and impure parts of our code.getLine
is impure because its result value is not guaranteed to be the same when performed twice.”
getLine
returns IO String
(IO[String]
in Scala), and as the author (Miran Lipovača) states, it is impure.
Quote from Martin Odersky
In my book, Functional Programming, Simplified I share this quote from Martin Odersky (from this discussion) that echoes my own opinion:
“The IO monad does not make a function pure. It just makes it obvious that it’s impure.”
Two practical benefits of using IO monad in Scala
As a practical matter, there are at least two good things about an IO monad. First, as just stated, it makes it obvious that a function is impure. Indeed, unlike using Try
, it also makes it very clear that some sort of I/O with the outside world is involved. Because function signatures in functional programming are so important, it really is a big benefit when you can look at a function’s API and see a signature like this:
def foo(s: String): IO[String] = ... ----------
Right away you know the function is impure and does something to interact with the outside world.
A second practical benefit is that if you have a series of functions that all return the IO
type, you can easily chain them together in a for-expression. This benefit comes from IO
implementing map
and flatMap
methods, which enables it to be used in for-expressions.
Summary
I started thinking about this topic again recently after reading the Can someone explain the benefits of IO to me Reddit post from last week. As I mentioned earlier, I think it’s a disservice to people who are trying to learn FP when they read that the IO monad somehow makes a function pure. From my own experience I can say that reading statements like that really slowed down my learning curve as I kept struggling to understand how IO
made a function pure.
It was only when I focused on the real-world benefits of the IO monad — a) it makes it obvious that a function is impure and interacts with the outside world, and b) you can sequence a series of IO
functions together in a for-expression — that I was able to get past the pure/impure argument and benefit from using IO
.
Lastly, as I wrote in my book, I encourage you to question everything.