Scala's Gamble with Direct Style

This is a more superficial article, i.e., an opinion piece. I’m making it my mission, again, to publish more thoughts on my own blog.
Scala has had a wide ecosystem for functional programming™️ directly inspired by Haskell and OCaml, due to its expressive type system and support for both type class encodings and OOP. As such it has inspired not one, but multiple runtimes for monadic IO, such as Cats-Effect, ZIO, Kyo. And these libraries served as an inspiration for others, see Effect-TS.
Yet, Scala 3, the language, does not move in the direction of more monadic IO, but rather in the direction of “direct style”, preferring continuations to monads, but without providing support for continuations out of the box. There are some attempts to fill that gap:
- The dotty-cps-async approach is IMO the best bet, even for retrofitting monadic IO to direct style. But in my limited experience, it has edge cases, and in the projects making use of it, like Cats-Effect (possibly ZIO or Kyo as well), this support isn’t used much. For example, one reason this happens is because the interruption model is different from that of the JVM, e.g., if you don’t turn a cancellation into an
InterruptedException
, it won’t blend well with the language’s constructs, liketry-catch-finally
. But also, support not being part of the actual language, in this case, may mean the implementation is cursed to battle edge cases and bugs forever. - gears builds on top of virtual threads for the JVM, and it supports Scala Native as well. It can support WASM via WASM JSPI, possibly in the next release. So it builds on the runtime’s support for either blocking threads or (one-shot) continuations. That’s incredibly limiting. For example, it has no JavaScript support and blocking threads is still very taxing on JVM-like platforms that don’t have virtual threads, such as Android. And obviously, execution cannot be fine-tuned, and you don’t have the abilities of a user-space runtime such as that of Cats-Effect or ZIO.
- ox is a JVM library for blocking threads and doesn’t make any attempts of supporting anything else but the JVM. If you work on the JVM, I guess that’s fine, although the library will be somewhat less useful after Structured Concurrency lands in Java 25. If we want to go back to blocking threads (with mostly the same caveats applying), I guess that’s acceptable with the support for virtual threads since Java 21, but what about Scala Native, JS or Wasm? Ox is a cool library and may be right for a lot of projects, but for the ecosystem, if Scala doesn’t escape the JVM, or at least JVM-isms, it’s far less interesting than the next versions of Java (which will also have type-classes BTW).
Keep the above in mind and compare with Kotlin:
- Kotlin Coroutines have multi-platform support, and this means — JVM, Android, iOS (Native), JS and WasmGC.
- Watch Structured concurrency by Roman Elizarov to get a sense of the design considerations that went into it. This isn’t your grandpa’s async/await.
- Those coroutines, along with its support for context parameters, can be leveraged not just for I/O, but also for handling of resources in general, matching Scala’s libraries such as Cats-Effect and ZIO (see Arrow).
- Multiplatform support is top-notch. It has a growing ecosystem of libraries, with Compose Multiplatform having stable iOS support and getting contributions from Google and others.
- It’s a language that evolves as well. For instance, context parameters are almost here, rich errors as well, and it may even get better immutability support before Scala.
Scala obviously relies more on community and less on commercial support. I actually loved that about Scala, despite it being unable to remain viable for targeting mobile devices, but…
Scala 2.x thrived due to making it saner to work with asynchronous I/O and concurrency. And yet the world isn’t standing still and alternatives have improved. Scala could’ve taken the path of F#’s computation expressions, thus improving the ergonomics of working with monadic IO. Scala could also include support for continuations out-of-the-box. Scala 3 does neither of those things, which means monadic IO is not getting the support it needs in order to go mainstream and the “direct style” approaches are currently in limbo.
I understand why Scala’s designers may prefer a “direct style” path. I’m not sure if I believe in the long-term success of monadic IO. Undoubtedly, it’s currently a marketplace failure, and when you solve concurrency issues by other means, it’s debatable if monadic IO is still worth it. Even those that swear by Effect-TS have to realize that its apparent success only has to do with how much async/await/Promise in JavaScript sucks, and if that pain is ever fixed, the project’s growth will likely stagnate.
But Scala’s evolution is currently alienating the part of the community that builds cutting-edge, user-space I/O runtimes that are the envy of the industry, while not providing the support required for making “direct style” work for the folks that would rather prefer that over monads. And I think that’s bad, especially as alternatives exist, one of those alternatives being Java 25.
I still ❤️ Scala, it’s a productive language, and I believe it will be even more awesome with capture checking. But making programming safer is just one aspect of what makes or breaks a language. There are other aspects, such as the platforms and problem domains a language is able to target. Most problems we solve on a daily basis are I/O-related problems and without a consistent story that targets the mainstream, I fear for its future.