The Great Concurrency Smackdown: ZIO versus JDK by John A. De Goes

Alex Otenko oleksandr.otenko at gmail.com
Wed Mar 15 09:34:35 UTC 2023


Perhaps, the most underestimated part of functional programming, is the
ability to braid monads.

So, you can turn F[G[A]] into G[F[A]] for some F or G. For example,
Timeout[Sequence[A]] - the type of expression for timeout wrapping a
sequence of statements that returns a value of type A - can be turned into
Sequence[Timeout[A]] - a sequence of statements each potentially timing out
while computing a value of type A.

The thing is, you don't need to reimplement Sequence or Timeout. You just
get it for free [supposing there is a monad transformer for Timeout]. This
manifests itself in the ability to literally stop the parallel
computations, when one finishes, or timeout arrives.

You can't get this in old school programming languages. For example you
have to modify blocking APIs to support timeouts - all of them. Then you
have to modify blocking APIs to support cancellations - all of them. Etc,
etc.


Alex

On Wed, 15 Mar 2023, 08:48 Alex Otenko, <oleksandr.otenko at gmail.com> wrote:

> Re: race and leaks
>
> I think he is referring to the fact that even if you have a timeout, the
> tasks that haven't completed keep running. Maybe he is mistaken about it,
> but that's how I understand it, and as far as I can see the current
> blocking APIs don't solve this problem.
>
> The claim is that various effects, of which race and termination are only
> two examples, can be captured by the type system and allows for better
> control of such effects.
>
> This dig, of course, is not about Loom at all. This is about the old
> school programming languages where sequence of statements has so many
> effects, but no way to control them.
>
>
> Alex
>
>
> On Tue, 14 Mar 2023, 17:27 Ron Pressler, <ron.pressler at oracle.com> wrote:
>
>> Hi Eric.
>>
>> I watched the talk and was able to find just one valid point in it. The
>> rest fall
>> into three categories: 1. lack of familiarity with Java's features 2.
>> lack of familiarity
>> with imperative composition, and 3. a misunderstanding of what it is
>> that virtual
>> threads (or even the JDK in general) let you do. The points in the first
>> two categories
>> are, I think, erroneous; the ones in the third category are mostly
>> irrelevant.
>>
>> The first category first:
>>
>> 1. That InheritableThreadLocals have less-than-ideal overhead is one of
>> the
>> reasons we've added ScopedValues (JEP 429), which are designed to
>> interoperate
>> well with structured concurrency.
>>
>> 2. Interruption is as reliable as anything that can be offered in either
>> Java or
>> Scala. The mechanism could be made more friendly, but I don't think
>> that's what
>> he's talking about. It may not be as reliable as what Erlang can offer
>> thanks
>> to its special restrictions on the language.
>>
>> 3. StructuredTaskScope does not leak threads. Closing the scope waits for
>> all
>> threads to terminate.
>>
>> 4. StructureTaskScope's error handling isn't lossy. I think that's
>> misunderstood
>> partly because he hasn't closely looked at the API and partly because he
>> missed
>> the significance to observability of every fork running in its thread; I
>> hope
>> that significance will become clearer in JDK 21. In short, because every
>> task
>> is its own thread, it can report exceptions to interested observers with
>> an
>> observable context; as a result, the parent only needs to be made aware of
>> exceptions that directly affect it, but it need not offer any additional
>> assistance in observing the forks. Nothing here is lossy -- all
>> exceptions are
>> reported and the scope is always made aware of any fork exception that
>> could
>> impact it.
>>
>> 5. The point about threads not being “type-safe” misses the fact that in
>> Java
>> threads are not used via the Thread API directly, but through
>> ExecutorService
>> APIT (or the new StructuredTaskScope). Something could be said about
>> checked
>> exceptions, I guess, but that’s a completely separate topic, a hard one,
>> and not
>> one where the asynchronous-functional approach particularly shines,
>> either.
>>
>> 6. The TwR construct in Java does not require nesting in the form shown
>> in the
>> talk (the speaker seems unfamiliar with Java's TwR syntax). However,
>> there is a
>> good point here and that is that AutoCloseables are not composable, which
>> requires work to support dynamic resources. We’re working on designing a
>> more
>> capable, and composable, interface than AutoCloseable to work in TwR, but
>> it’s
>> not super-high priority at this time. That was the only valid point I
>> managed
>> to find in the entire talk.
>>
>> The second category is exemplified by the speaker's mistake about the
>> composability of the `race` method he presents. This is how obvious the
>> composition is:
>>
>>     <T> T withTimeout(Instant deadline, Callable<T> task) throws
>>     TimeoutException, InterruptedException, ExecutionException { try (var
>> s =
>>     new StructuredTaskScope.ShutdownOnSuccess()) { s.fork(task); return
>>     s.joinUntil(deadline).result(); } }
>>
>> It can be composed with `race`, or any operation, anywhere in the
>> hierarchy.
>> I.e., instead of `race(tasks, deadline)` you can write `withTimeout
>> (deadline, race(tasks))`.
>>
>> He tries to explain the "composition deficiency" he believes he detects by
>> pointing out that statement composition is not a composition of values.
>> But
>> someone who thinks about functional effects should immediately see the
>> theoretical error here because statement composition is not functional
>> "Unit"
>> composition, but rather monadic composition; and the monad here is reified
>> where (in other words, where is the `bind`)? In the thread. When an
>> operation
>> blocks it effectively returns a monadic value to the scheduler, which
>> exposes
>> the means by which the operation be transformed and composed with, say, a
>> timeout. That's why a functional effects person should immediately
>> suspect that
>> if they can't see how the transformation can be applied to Java code they
>> must
>> be missing something. Examination of the code for `race` itself would
>> quickly
>> show how this transformation can be factored out of `race` and abstracted.
>>
>> The final category is exemplified by the truly bizarre point about
>> retries/backoff and loops. That virtual threads allow implementing retries
>> using loops doesn't mean that that's what user code looks like. I would
>> think
>> this is almost too obvious to state, and yet what's shown in the talk is
>> not
>> how programming works in virtually any high-level language or any style.
>> Rather, that retries can be efficiently implemented using loops simply
>> means
>> that by selecting the right building blocks we managed to avoid adding
>> retries
>> as a primitive and can allow such a general abstraction to be written in
>> a way
>> that is 1. composable with any I/O library and 2. interacts well with the
>> platform's observability. That's what a win is (at least for a language).
>>
>> Boasting of a library where retries are primitives, they cannot compose
>> with
>> other libraries, and they do not interact well with observability is just
>> puzzling in this context. Thanks to virtual threads, libraries can offer a
>> truly composable and observable retry construct (because they can be
>> implemented with something as simple as a loop). He further boasts that
>> the
>> implementation of a synchronous `retryWithExponentialBackoff` library
>> routine
>> won't fit on a slide, even though it would still be shorter than an
>> asynchronous implementation...
>>
>> Having new code specificially written for one framework compose with
>> other code
>> written for that framework is a very low bar to clear, and it can only
>> work if
>> you manage to attract the ecosystem over to program with your new
>> framework;
>> virtual threads were added to Java only after a lot of people complained
>> that
>> that approach doesn't work -- it doesn't compose with the code they
>> already
>> have and requires too much rewriting, it doesn't work with their tools,
>> and too
>> many of them simply don't enjoy it. What virtual threads do, on the other
>> hand,
>> is allow writing concurrent code that composes with virtually any other
>> simple
>> synchronous code ever written and that will ever be written for the Java
>> platform, and with the same observability and debuggability Java
>> developers
>> expect. We're able to do that not because we're smarter than library
>> authors,
>> but because these things require deep platform support. I think it won't
>> be too
>> long before we know whether or not our approach works.
>>
>> The JDK — a language and runtime platform — hasn't ever intended to
>> eliminate
>> the need for third-party libraries, nor does it now. Quite the opposite:
>> we try
>> to include as little as we can get away with. The JDK's goal is to
>> provide the
>> right building blocks. None of the features produced by Loom to date --
>> virtual
>> threads, structured concurrency, scoped values -- could have been
>> implemented
>> by libraries, as they all required changes to the platform. They are the
>> building blocks on top of which third party libraries -- including those
>> that
>> may provide "Refs" -- can be written in a way that is more composable and
>> observable than was ever before possible on the Java platform.
>> Conversely, I
>> don't believe Loom has contributed any feature that could have been
>> implemented
>> in a third-party library; we've so far focused only on the primitives.
>>
>> Having said all that, it's important to remember that he's talking about a
>> library that's aimed at programmers who like different things than Java
>> programmers so much so that they use a different language, and, I'm sure
>> at
>> least some of the high-level libraries will continue to cater to those who
>> aesthetically prefer the functional style in whatever language they enjoy
>> programming for the Java platform.
>>
>> The important question that wasn't asked in the talk at all is, "how
>> should
>> high-level concurrency libraries work on top of the JDK now that we have
>> virtual threads?" I think that one big difference is that thanks to the
>> improved composability, high-level functionality that used to be offered
>> by
>> monolithic frameworks could now be offered, a la carte, by more focused
>> libraries.
>>
>> As to your question, the for-yield construct is just syntax for monadic
>> composition that's already built into Java's imperative composition, but
>> as for
>> `foreachPar`, I believe we can do even better. We're already working on a
>> way
>> that will lead to a structured concurrency construct that works with
>> streams or
>> something similar to it. JEP 428 explicitly states that
>> StructuredTaskScope may
>> not be the only structured concurrency construct we'll introduce.
>>
>> -- Ron
>>
>> On 10 Mar 2023, at 22:57, Eric Kolotyluk <eric at kolotyluk.net> wrote:
>>
>> The Great Concurrency Smackdown: ZIO versus JDK by John A. De Goes
>> <https://www.youtube.com/watch?v=9I2xoQVzrhs>
>>
>> I would recommend this presentation as interesting, worth watching.
>>
>> However, I found a lot of problems with the arguments, and while John De
>> Goes knows a few things about Loom, his knowledge is incomplete and out of
>> date. On the other had, he does make a few interesting claims about Zio
>> that *might *be nice features in Loom some day. In particular, the
>> for-yield structure in Scala and Zio is a very powerful structure, although
>> can be quite cryptic to fathom, especially by the most clever in the Scala
>> community.
>>
>> I will withhold my other insights and questions until people respond to
>> this, showing interest in further discussion.
>>
>> Cheers, Eric
>>
>>
>>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20230315/8a24d28b/attachment-0001.htm>


More information about the loom-dev mailing list