> “The IO monad does not make a function pure. It just makes it obvious that it’s impure.”
https://groups.google.com/forum/#!topic/scala-debate/xYlUlQA...
> “The IO monad does not make a function pure. It just makes it obvious that it’s impure.”
https://groups.google.com/forum/#!topic/scala-debate/xYlUlQA...
fn() + fn()
// ----
val a = fn()
a + a
Are the two programs above equivalent? For a function returning IO, they are. For a function that's side effectful, they are not.(As an exercise for the reader, think of our function here returning Future/Promise instead of an IO, how would that change the program?)
This is a definition of purity that you can work with. The trap that people fall in is to talk about fiction essentially. What is "meaningful" anyway? If you don't give it a definition, is it meaningful to talk about what's meaningful?
I agree that a function returning IO is opaque. You can't reinterpret it, it's no longer a plan that you can change the interpreter for, but something concrete. There are of course better abstractions for dealing with effects in a way as to make them less opaque ... like the Free Monad, or Eff.
Many of them have been expressed in Scala as well.
In a typical IO function, `fn()` only gives me 10% of the code in `fn`: the part up to the first bind. To get the rest, I need to `do` it, and this is where purity is no longer meaningful:
a <- fn "a"
b <- fn "b"
Can I swap these two lines ? The IO monad tells me there might be consequences, but what are they ? Another example: foo fn
Will `foo` trigger the IO effects of `fn` zero, one or more times ?I believe these are meaningful questions, and yet, a program built from pure functions can answer them no better than an imperative program.
Agreed. However having referential transparency for IO does help. Let me explain...
The program you mentioned has one clear property: those two computations will execute sequentially.
This sequence of executing operations becomes independent of evaluation via IO. And if you want parallelism, you need to make it explicit.
val a = fn(1)
val b = fn(2)
for {
r1 <- a
r2 <- b
} yield r1 + r2
For IO this always has the same execution characteristic. "B" will execute after "A". You have a clear happens-before relationship.As an exercise:
1. think of what happens if that function returns a Future/Promise
2. think of what happens if that function is synchronous, but still side effectful
In both cases the actual order of execution is not guaranteed. In the second case the ordering is only guaranteed from the point of view of the current thread only. And this is interesting b/c the only way to force ordering in a multi-threaded context, besides memory barriers which are a runtime trick, is to create a data dependence. In other words, while this code doesn't guarantee visibility with IO, it does have stronger ordering guarantees than any side effectful code you can throw in there.
You're a little unfair here btw. If the two operations here are not dependent on each other, then the Monad's flatMap/bind isn't the operation you want, since Monads literally describe sequencing. And you want sequencing in case order of execution does matter.
In other words, when you see a sequence described via bind/flatMap, you have to assume that the order is relevant, because the author could've done this instead, which is self explanatory:
(a, b).parMapN { (r1, r2) =>
r1 + r2
}
And now they are executed in parallel, with no related evaluation gotchas (exposing the API in the Cats library here).So to say that IO doesn't give you any useful property here is factually untrue. The obvious gain is that you look at an expression and know exactly how and when it will execute.
https://typelevel.org/cats-effect/datatypes/io.html
Also see my comment here: