Quiet for comprehensions

Interesting idea; one potential downside is that with traditional do notation it is very clear what is the scope of the effectful code:

// hypothetical syntax
def userLogin(username: String, password: String): Future[UserInfo] = do
  val dbReq = CheckUserLoginRequest(username, password)
  val userId  <- userDb.login(dbReq)
  val profile <- profileDb.fetch(userId)
  val avatar  <- avatarDb.fetch(userId)
  UserInfo(profile, avatar)

In the proposal you’d need to scan the function for do occurrences, no?


What would be the proposed desugaring for this?

if do isUserActive then foo else bar

It would be awesome if Scala improved upon the current state of for-comprehensions.
We don’t have to look far for inspiration, we can do something similar to what F# is doing with so called Computation expressions

For Scala the code then could look something like this

def userLogin(username: String, password: String): Future[UserInfo] = for
  val dbReq = CheckUserLoginRequest(username, password)
  val! userId = userDb.login(dbReq)
  val! profile = profileDb.fetch(userId)
  val! avatar = avatarDb.fetch(userId)
  yield UserInfo(profile, avatar)

A general de-sugaring table could be like this

ordinary code analogue for-comprehension sugar for
val ... = ... val! ... = ... (legacy ... <- ...) flatMap
if ... then ... else ... if! ... then ... else ... ifM
if ... then ... if! ... then ... whenM
throw ... throw! ... raiseError
try ... catch ... try! ... catch ... handleErrorWith
while ... do ... while! ... do ... whileM
use ... = ... (hypothetically) use! ... = ... use

The point here is to

  • enable as many analogues to syntactic constructs from ordinary code (val, try, if, …) as possible
  • make them look as similar to ordinary code constructs as possible. Appending ! is one possible way to do that. val! is better analogue to val than <- can ever be.

Previously discussed on this forum:

On Reddit:


A poorly-fleshed out thought: this seems like it could be a generalization of Rust’s ? for early return to the Future monad. If do or whatever syntax was rewritten by the compiler a local return for general monads, that would be a huge win for the language in general by making Result much easier to use and would get this behavior “for free” for Futures too.

I’m imagining a method called def orElseReturn: Either[T, U] or something that gets specially desugared by the compiler to a local return just like in Rust. If the function returns a Right then the U is the result of the expression, otherwise the compiler returns out of the surrounding function with a the Left T.

I’m sure this has been talked about before elsewhere.

I recommend watching Odersky’s full presentation on Direct Style Scala.

As discussed,

  1. This effort brings continuations to Scala, not which solves many, many more problems than async-await. Examples include error handling, effect tracking, concurrency, and more.
  2. Direct-style programming is simpler than the monadic style and composes effortlessly. There’s no need for monad transformers or higher order methods like traverse. You will be able to set breakpoints and debug your code easily (at least on the JVM with project Loom). It dramatically simplifies your code.

Direct Style is not just a replacement for monadic for-comprehensions but is a whole new paradigm that requires time to fully appreciate.

Your example might look like this in direct-style:

def userLogin(username: String, password: String): Future[UserInfo] =
    val dbReq = CheckUserLoginRequest(username, password)
    val userId = userDb.login(dbReq).value
    val profile = profileDb.fetch(userId).value
    val avatar = avatarDb.fetch(userId).value
    UserInfo(profile, avatar)

Which is similar to the suggested for-comprehension change, but more general.

If this code is executing on the JVM, we can do even better. Assuming we are using a Virtual-Thread-based web framework like Helidon Nima, your userLogin method is already being called on a Virtual Thread, so we can remove Future entirely (since blocking is free on Virtual Threads). userLogin then becomes

def userLogin(username: String, password: String): UserInfo =
    val dbReq = CheckUserLoginRequest(username, password)
    val userId = userDb.login(dbReq)
    val profile = profileDb.fetch(userId)
    val avatar = avatarDb.fetch(userId)
    UserInfo(profile, avatar)

The fact that monadic effect systems might become easier-to-use under a proposal like the one laid out in this thread doesn’t preclude other types of effect systems from being brought into Scala, including those not based on pure functional programming.

In what way are continuations not pure functional programming?

I think ZIO and CE can adapt to direct-style and greatly benefit their users in the process. The syntax will be easier to write and debug, but retaining the same laziness and concurrency operators. Scala-fix can help to migrate for-comprehensions to direct style.

1 Like

In both your examples profileDb.fetch & avatarDb.fetch will execute sequentially when most likely they should be executed concurrently. Executing them concurrently will require additional bells and whistles (like zip operator) when by all means it should be done automatically like compiler/processor instructions reordering.

Also, no examples of cancellation and RAII where given in the mentioned presentation. And structural concurrency, cancellation and resource allocation/release are the most interesting things in effects system like CE or ZIO (at least for me).

1 Like

To have them execute concurrently, you could rewrite the example as:

def userLogin(username: String, password: String): Future[UserInfo] =
    val dbReq = CheckUserLoginRequest(username, password)
    val userId = userDb.login(dbReq).value
    val profile = profileDb.fetch(userId)
    val avatar = avatarDb.fetch(userId)
    UserInfo(profile.value, avatar.value)

Yes, but it is the very same pitfall we have now with Futures: subtle differences between val a: Future[?], def b: Future[?], etc.

A lot of people do not know the difference between

  a <- foo
  b <- bar
yield (a, b)

val fa = foo
val fb = bar

  a <- fa
  b <- fb
yield (a, b)

And now we gonna shoot the same leg with different bullet?


Yes, but it is the very same pitfall we have now with Futures: subtle differences between val a: Future[?], def b: Future[?], etc.

This is because Future is eager; it’s tangential to the direct-style discussion.

You could easily write a lazy Future (like ZIO or CE) that supports direct-style.

Hopefully, Virtual Threaded web frameworks become popular, and the majority of users get async performance without having to directly use Future or any concurrency monad again. Those that do need to write concurrent code can do so much easier with direct-style, whether it is with an eager or lazy concurrency primitive.

That’s something we do not know. Presentation says that “direct style continuations” are very much WIP and who knows if there would be support for .? and .value in user land?

In Rust, which was used as an example, ? is possible to use only with Rust own Option and Result and async/await – only with Rust own Future. No support for a third party types.

Any adoption of VTs are depended on JVM 21 LTS. Which is at least September 23. And looking at numbers of Java 11/17 adoption we are talking about 2030. Scala itself supports Java 8.

So any direct or monadic style needs to work with what we have now and (maybe) switch to VTs for runtime if they are available. But the interface must be universal.

And I will say it again: cancelation and RAII. Project Loom does nothing about it, so constructs like try-with-resource/Using should be at least compatible with direct style.


I want to throw out one word of caution here. One of the common complaints about Scala, especially from the standpoint of educators, is that there are too many ways to do the same thing. Every new special syntax for things provides another argument for some not to adopt Scala as a teaching language. I understand that many of these options improve lives for professional developers, but there are also costs associated with having special syntax that I want to make sure are being considered.


Yep, agreed. That’s one of the reasons my suggestion is as conservative as it is: it’s just a modest simplification of the existing syntax, that I think is a bit easier to teach. It’s aimed at beginners at least as much as experienced engineers. (If it worked, I suspect that for would become the “advanced” syntax.)

I agree, but I think it goes beyond teaching. You want code that’s readable. Yes, we want to avoid boilerplate and we want powerful abstractions. But by hiding too much, you run the risk of making code harder to read for outsiders. I’m not judging this particular proposal, but, as a general principle, let’s not be too quiet and obscure the code’s intent for the sake of typing fewer characters.


The thing that I don’t understand is how this is an improvement over a purely library-level solution that is slightly more explicit but in a Li Haoyi-like style where simplicitly of the most-used path is an extremely strong goal.

For example, if you write

def userLogin(username: String, password: String): Future[UserInfo] =
  val dbReq = CheckUserLoginRequest(username, password)
  val userId = future:
  val profile = future:
  val avatar = future:
    UserInfo(profile.?, avatar.?)

even if I don’t tell you what anything means or how it is implemented, isn’t it clear what is going on?

If you forgot a .? and the compiler complained about needing Foo instead of Future[Foo], then it would be even clearer. No?


@jducoeur you may take a look to more powerful solution with a sample here Transparent Monads syntax and Monadic Flow Control interpretation - #6 by Shaman

Loom currently doesn’t support custom executors and, most likely, never will.
That means that any effect system which decides to base its fibers on top of the Looms VTs will be forced to use the FJP from which both cats-effect and zio moved away because of reasons including performance ones.

Besides performance and already mentioned RAII and cancellation problems - custom executors allow way more control over effect runtime, and losing this control adds even more headache to full migration to VTs.

This wouldn’t be the problem if Loom exposed their continuation directly. But currently, there are only ephemeral plans without anything specific.

Also, as a side note, I saw some developers who think that syntax presented by Odersky is even more cryptic than the current.
And I partially agree with this statement - I can see how abilities shown in the “acrobatics” example can be abused.


A small addition to VTs problems - what to do with other platforms besides JVM?

If, in the case of SN, there is some work in progress to implement Loom features using the LLVM coroutines, then what to do with SJS isn’t clear.

1 Like

Actually LLVM coroutines are not a best candidate to implement Virtual Threads. LLVM only can provide us with stateless coroutines, these cannot be yielded in arbitrary point of nested calls, however they might might be useful for Kotlin like async-await, or C/Python like generatorw. However there is an ongoing work on adding stackfull coroutines, which would be able to work as a foundation for green threads/virtual threads.

1 Like

That sounds interesting!

What’s the design? Where can we learn more?