> "
Purity is not a goal in itself"
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.