Hello folks,
I promised you in How to live in a pure world I would have written a full article showing an example of how to deal with IO functiona...
For further actions, you may consider blocking this person and/or reporting abuse
Suspended functions can be a replacement for all monads because continuations are the mother of all monads.
We could define NonDet and Either exclusively in terms of suspension. We are gonna bring suspension and imperative FP to all data types not just IO as described in this excellent article by @eureka84 and eliminate as much nested style as possible to make FP truly ergonomic in Kotlin.
We have already built shift/reset and a polymorphic ContT over suspension exclusively and two primitives that allow for a la carte strategies per datatype wherein strict ones we can simply fold and suspended ones suspend and resume in any thread.
Continuation also let us flatten three layers of transformers so binding is interleaved.
you can bind in place io, either, io of either and either of io.
nobody is doing this and there is no need to lift to a particular context.
The ergonomics of continuations and the duality of imperative programming has been historically disregarded because most langs have been copying the indirect Haskell encoding. Nothing wrong with that but the JVM prioritizes an imperative stack and labelled loop over reified data structures that need to be folded and interpreted as in most IOs in the JVM except Arrow Fx Coroutines.
But what I would like to bring to your attention with this comment is that suspend is not a replacement for IO but all monads and foldable (see SequenceBuilder in the std lib) and can support interleaved transformer binding at all layers of the transformers, also effect handlers and really anything since it isn't restricted to monads. We have proven monoidal builders as well and there is more we are researching drastically how FP is seen from Kotlin and it's all thanks to the suspension and compiler CPS transformation
Thank you Angelo again for this excellent article, we don't talk enough about these things.
Jannis, Simon and I are currently working on this if anyone wants to get in touch and find out more and get involved. Cheers!
You write that Kotlin can avoid monadic nesting in some/many cases using delimited continuations based on suspend functions. I am currently researching on this topic in the JS context, where we have generator functions. The problem is that such functions can only resume once at a specific position, but to flatten monadic computations in general we'd need multi-shot continuations. How do you solve that in Kotlin? Are Kotlin suspened functions multi-shot? Maybe you can provide some external resources?
Think I answered my own q: The Kotlin compiler performs a CPS transformation under the hood, similar to Haskell's
do
block transformation to nested>>=
.yes and while multishot is not supported since they are single-shot delimited, you can still multi prompt and build multi-shot like continuations with it.
This is very interesting to me. Can you provide further resources?
schoolofhaskell.com/school/to-infi...
blog.poisson.chat/posts/2019-10-26...
gallium.inria.fr/~scherer/research...
And check out the multiple impls of ContT. Here goes one in swift:
gist.github.com/sjoerdvisscher/a56...
This great discussion about them in Kotlin gist.github.com/elizarov/5bbbe5a3b...
( we got passed the multiprompt issue thanks to Jannis but have not had time to update the combo)
Hope this helps.
Thanks for taking the time. Kotlin seems to be very interesting in the sense of a different approach to FP than Haskell.
Suspended functions seem to be a replacement for the IO type, not the IO monad. A more idiomatic IO type for Kotlin so to speak. You probably can write a monad instance for suspended functions, but I am not that familiar with Kotlin so I may be wrong.
This sound like Haskell's
main
function, which is always of typeIO
and interprets at runtime what theIO
actions mean in the real world.Compared to Haskell suspend functions would be like lang special syntax support for something like ContT. But suspension can be generalized to other monads. See the Kotlin std lib SequenceBuilder which is unrelated to IO and builds collections lazily. See also this comment for a more in-depth look into what we are doing dev.to/raulraja/comment/13a8h
Yeah, the intent is to become more idiomatic.
I don't know much about Haskell (just read a bit "Learn you a Haskell for Great Good" but never actually used it).
Thanks for the post! I'm coming mostly from a Scala background, so it's good to get to know what's happening in Kotlin, especially that Java seems to be taking a similar route with Loom.
I've got a question though about the programming model. When working with
IO
, we're working with side-effect-free/pure functions, and get a number of benefits like referential transparency (we also get some non-benefits, but that's covered in your article). However, when we shift to working with suspended functions, don't we once again move back to working with side-effecting code?That is, moving from IO to suspended functions would mean moving from FP to imperative/procedural programming. (And maybe that's fine - for performance and readability reasons.)
Another question is can you "lift" a suspended function to a value? While I see how suspended functions provide for much more readable code when it comes to sequential logic, I think for writing concurrent programs (even something as simple as running computations in parallel and combining their results) the lifted representation might be better.
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.
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
(whereT
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 justT
. If I would e.g. want to race a two processes, I would write something likeval 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 changeresult1
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.In your example about race keep in mind that saying it takes
suspend () -> A
is equivalent to taking as inputIO[A]
(as far as I understood).For a better explanation read here, especially the sections
Arrow Fx vs Tagless Final
andGoodbye Functor, Applicative, and Monad
.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 :)I have high expectations from Arrow.
Currently, I cannot consider it as a viable option for production applications, simply because library is not mature and is changing drastically with each version (0.7, 0.8, 0.9, 0.10, 0.11).
This is a good thing because innovation requires disruptive changes. The idea to use idiomatic Kotlin to represents monads is great and most importantly would make more appealing the use of the FP among imperative programmers.
At some point we need though a milestone version
This is coming up this year when the Kotlin IR backend is stable in the 1.4.x series. We almost feature complete for 0.11 and stable comes after that one.
One of the things I've grown to really like about F# is every function is a monad.
I've even implemented some monad magic in C++, but it isn't idiomatic C++ because functional programming is not idiomatic C++.
The critical insight for I/O is that your output is only a side-effect if it can influence your input. :)
(see here)