PRE-SIP: Suspended functions and continuations

TLDR: This pre-SIP proposal to add “suspended functions and continuations” to Scala 3 needlessly imports problemmatic legacy Kotlin approaches to solving a problem that Loom already solves in a theoretically more performant and user-friendly fashion.

In this discussion, I’m going to ignore “side-effects” since it is my firm belief that “side-effect tracking” is not commercially relevant, and if it were (which it’s not), it would have another solution that is not connected to the challenges around async programming.

Kotlin attempted to solve the async problem by introducing language-level coroutines, which are implemented by specially translating functions marked by suspend into computations that may asynchronously suspend through (essentially) “saving” JVM stack to the heap.

This solution allows Kotlin to support async programming without the level of ceremony and boilerplate common in “reactive” solutions, including those based on monads. More importantly, it allows Kotlin to support direct imperative style, which is familiar to nearly all programmers and requires no training.

Unfortunately, what was once an advantage for Kotlin has become a liability and legacy decision: Kotlin chose to solve the async problem by modifying the Kotlin language, rather than the Kotlin runtime.

This means that information on which functions are async leaks into the programming language syntax itself. Kotlin has re-invented the colored functions problem, with all of its well-known drawbacks.

An alternative solution to the async problem would have been to bake async into the Kotlin runtime, rather than the language, essentially by reinventing virtual threads, which would have allowed Kotlin developers to write Kotlin code that is obvlious to whether it is async or sync.

Unfortunately for Kotlin, the JVM innovated at a rapid pace that few could predict, and now Loom has given us full async programming in a direct imperative style, without the need to mark any functions. Loom also transparently works with all legacy code, making it fully modern and asynchronous (with a few exceptions such as synchronzied and file IO).

Kotlin is now in a weird situation where its (at the time) forward-looking support for async programming has become legacy–a solution that is inferior in terms of maximum theoretical performance, as well as user-friendliness, to the virtual threading solution powered by the Loom advancement of the JVM.

Scala 3 can already support fully async programming with no additional syntax or compiler passes, merely by running on a post-Loom JVM! In this world, we are able to write fully asynchronous code in a direct imperative style, without introducing introducing the two-colored functions problem, or importing Kotlin’s (now legacy) solution to the problem.

Now, in the spirit of fairness, I should point out that advancements in Loom do not help Scala.js or Scala Native. For all of the good that Loom does, its benefits are restricted to the JVM. Currently, the vast majority of all commercially relevant Scala development occurs on the JVM. While I love Scala.js and Scala Native and support their development, these markets are tiny and will remain so for the foreseeable future. The JVM is where all the commercial dollars are going and where Scala’s strengths remain.

In my view, the Scala.js and Scala Native markets are insufficient to justify any language-level changes to the async problem. If those markets were commercially relevant, then that might be an argument to invest resources in solving the problem, but it should be solved in the way that Loom solved it, which is a substantially and objectively superior solution (transparent runtime support) to the way Kotlin solved it (two-colored functions). Under no circumstances would I recommend that Scala 3 copy the Kotlin model to support the Scala.js and Scala Native communities.

In summary, there is no reason to pollute the Scala 3 language with two-colored functions when Loom already solves the async problem in a way that is significantly better than the way Kotlin solved it.

Notes on Proposal

  • Addition of continuations is not necessary or helpful to achieve structured concurrency; indeed, the JDK itself will be getting structured concurrency primitives
  • Being able to eliminate Either is a non-goal given Scala 3 can leverage exceptions, which are compatible with direct imperative style
  • Union types and flow typing provide a nice way to reduce the need of Option and keep things simple
  • Non-blocking sleep and generators are already free with Loom
17 Likes