DEV Community

Discussion on: IO: to be a Monad or not to be, that's the question!

Collapse
 
eureka84 profile image
Angelo Sciarra

Hi Adam,
thank you for reading it! Well the thing is that suspended functions are not like usual functions and need a context (a CoroutineContext) to be run in.
In that sense they are special and can be considered like descriptions of a side effect because, as for IO monad and monads in general, once you work with a supended function it "infects" all calling functions, meaning you either provide a coroutine context, same as unwrapping the IO by running it, or mark also the caller function as suspended (use map or flatMap).
In that sense I don't think it fosters an imperative style (from a phylosophical point of view you could also say that Monads enable an imperative style in the FP world).
To answer your second question I think a suspended identity function is an equivalent of pure/point/return/just whatever you want to call it.
About concurrent facilities I invite you to have a look at arrow-fx-coroutines, it provides all the functions you may already know (tupledN, parMapN, raceN, and others) ant it is designed with suspended functions.

Hope to have provided you an answer.

Collapse
 
adamw profile image
Adam Warski

Thanks! This definitely makes sense - so a suspended function is automatically lazily evaluated, which is really what IO is under the covers (a lazily evaluated function () => T or () => Future[T]).

And you are right that monads enable imperative style in FP - nothing wrong with that I suppose, depending of course on the definition of imperativeness and FP (as these unfortunately aren't that precise). But imperative understood as running a sequence of steps is something that's very natural and common in programming.

Here's what I found for raceN. As far as I see, it's taking suspend () -> A parameters to avoid eager evaluation of the computations that are being passed in. So in a way, these values are double-lazy (one because of the suspension, two because of the () ->)?

So I guess (thinking aloud here) you could say that the representation of a computation as a value is suspend () -> T (where T is the result of the computation).

One problem that I would see here is that the representation of a computation isn't uniform. Sometimes it's () => T, sometimes it's just T. If I would e.g. want to race a two processes, I would write something like val result1 = raceN(() -> a, () -> b).

But if I want to race this with yet another computation at some point in the future, it's not enough to write val result2 = raceN(() -> result, () -> c). I have to go back and change result1 to be a no-params function. Thinking about it, I think I just described lack of referential transparency of the Kotlin solution.

Not sure how much of a problem it is in practice. But for sure it's some departure from what IO and "pure FP" (again, depending how you define FP) gives you.

Thread Thread
 
eureka84 profile image
Angelo Sciarra

In your example about race keep in mind that saying it takes suspend () -> A is equivalent to taking as input IO[A] (as far as I understood).

For a better explanation read here, especially the sections Arrow Fx vs Tagless Final and Goodbye Functor, Applicative, and Monad.

Thread Thread
 
adamw profile image
Adam Warski

Thanks for the link! I think my reservations come from the fact that with IO you have the following signature: race(a: IO[A], b: IO[B]): IO[Either[A, B]]. While with suspensions, you have: race(a: suspend () -> A, b: suspend () -> B): Either[A, B].

Note that the return type doesn't return our "effect type", that would be () -> Either[A, B], but an eagerly evaluated value (Either[A, B]). And this matters for composition, meaning that if you want to compose that process later with others, you'll have to keep that in mind when defining it.

Hence it seems we're trading the uniformity of IO and some composition properties for the better readability and performance of suspensions. As always, tradeoffs :)