Replies: 18 comments
-
I agree with your main statement. However my understanding is that "direct" Scala will move away from using monad types for concurrency. |
Beta Was this translation helpful? Give feedback.
-
Hey, thanks for the very detailed comment! It is true that async/await-style future is error prone for the reasons above, but we are not directly trying to build the same way. In fact, currently Our current "base" level asynchronous/suspendable function right now has the following form def fetchUser(using Async): User = ???
def fetchOrder(using Async): Order = ???
def handle(using Async): Response = {
val user = findUser()
val order = findOrder()
Response(user, order)
} where Here Btw, I recently mentioned whether we should encourage using |
Beta Was this translation helpful? Give feedback.
-
P/s: pretty much like Kotlin, to get the user and order concurrently one can wrap them in Futures: def fetchUser(using Async): User = ???
def fetchOrder(using Async): Order = ???
def handle(using Async): Response = {
val user = Future { findUser() }
val order = Future { findOrder() }
Response(user.value, order.value)
} |
Beta Was this translation helpful? Give feedback.
-
@natsukagami could you please give further details on |
Beta Was this translation helpful? Give feedback.
-
As for
|
Beta Was this translation helpful? Give feedback.
-
@natsukagami thanks for the response! I wrote this issue because The design is a little different from Kotlin's Coroutines. In Kotlin, if you write this, the compiler will trigger an error, in spite of this being marked with suspend fun handle(): Response {
val user = async { findUser() }
val order = async { fetchOrder() }
return Response(user.await(), order.await())
} The issue that the suspend fun foo(): Deferred<Unit> =
coroutineScope {
async {
runInterruptible {
Thread.sleep(2000)
println("Hello!")
}
}
}
fun main(args: Array<String>) =
runBlocking {
val d = foo()
println("Started foo")
d.await()
println("Finished foo")
} Except that this will always return a completed Deferred. And note how this is a small mistake that most of the time has an impact on performance, not correctness. Thus, the program's output would be:
Kotlin's equivalent for implicit parameters are the "context receivers". The suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R You can view that fun foo(): Deferred<Unit> =
GlobalScope.async {
runInterruptible {
Thread.sleep(2000)
println("Hello!")
}
} Basically, Kotlin makes it hard to leak unfinished def fetchUser(using Async): Future[User] = ???
def fetchOrder(using Async): Future[Order] = ??? Speaking of the cancellation model, the current design isn't adequate because it cannot wait for the completion of that cancellable. This can lead to leaked resources. I think this was already mentioned in issue #12, but it's worth repeating it with Kotlin's equivalent: public interface Job {
public fun cancel(cause: CancellationException? = null)
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
} In other words, the suspend fun bar() =
withTimeout(1.seconds) {
// This isn't cancellable; to make it cancellable,
// we could wrap it in a runInterruptible block.
withContext(Dispatchers.IO) {
Thread.sleep(10000) // sleep for 10 seconds
}
} Similarly, this too will also complete in 10 seconds, even though it could complete sooner if we cooperated with Java's interruption protocol by using suspend fun baz() =
coroutineScope {
// Not cancelable
launch(Dispatchers.IO) {
Thread.sleep(10000)
println("Done!")
}
// Not cancelable
launch(Dispatchers.IO) {
Thread.sleep(1000)
throw RuntimeException("Boom!")
}
} |
Beta Was this translation helpful? Give feedback.
-
I'm really tired to write any refs: Zig's suspend function : ziglang/zig#6025 When typing You can argue it's a bad idea, but Golang have many users just for their def fetchUser(using Async): User = ???
def fetchOrder(using Async): Order = ???
def handle(using Async): Response = {
async val user:User = findUser() //starting now, triggered by defined
async val order:Order = findUser() //starting now, triggered by defined
Response(user, order)
} and def fetchUser(using Async): User = ???
def fetchOrder(using Async): Order = ???
def handle(using Async): Response = {
lazy async val user:User = findUser()
lazy async val order:Order = findUser()
Response(user, order) //starting when polling values, trigger by pulling
} The |
Beta Was this translation helpful? Give feedback.
-
@natsukagami Both Zig and Dart are during the Design progress, maybe you can have some design ideas to share. |
Beta Was this translation helpful? Give feedback.
-
@alexandru Thanks for your comments! I am of course well aware of the history of futures and async/await, from the origins by Liskov to the implementations in a dozen languages. I am also aware of the notes on structured concurrency you linked to and think the concepts make a lot of sense. Like you, I don't think Scala's current futures should have much of a future, so it's fine if the industry really moves away from them. My argument against That said, the connection with our current effort in gears is much more nuanced than you state. First, I should state that gears is very much an explorative project, undertaken by some students and myself. It has nothing to do with Scala 3 and there are no plans to put it in the standard library or something like that. We want to find out how our ideas for a simple and straightforward system dealing with functional concurrency shape up as we venture into this further. Second, I note that some of of our differences might come from a difference in mindset. Your stated concerns are all about imperative effects. You rightly worry there might be a side effect in an unforced concurrent computation happening at unpredictable times that could be overlooked, and that would be really bad. I agree with the concerns, but I think that the stated problems with old-style futures will not apply to what we explore here (more on that below). My vision for futures in gears is much more functional in nature. Futures embody the principle of parallel computation. They are the most logical building block of lenient evaluation. We know about strict vs lazy: Strict evaluates once something is defined, whereas lazy evaluates once it is used. Lenient is the logical generalization when you have parallelism. It means a computation can start when it is defined and must finish when it is used. In between these two time points it can be scheduled at will. That's what a future is. And the first use case for a future is indeed some purely functional computation that we want to spawn out. We do not want to use a coroutine or process for that since we still want to observe termination (either normally or by an exception). So we need to sync on a result. This is the essence of dataflow computation (see also Morgan Stanley's Optimus Cirrus platform for a concrete realization of these ideas at scale). That said, one can discuss the details whether we want to explicitly wait on a future's result or whether this should be done implicitly. We are currently opting for explicit as the most straightforward conservative choice. Third, I believe the comments about unwanted concurrency are addressed in gears though the def fetchUser()(using Async): User = ???
def fetchOrder()(using Async): Order = ???
def handle(using Async): Response = {
val user = findUser()
val order = findOrder()
Response(user, order)
} No futures here. In fact, the closest analogue to a suspend keyword would be the With This is also reflected operationally. Scala 2 style async/await emulates stackless coroutines, so on every function call you have to snap back to And that also addresses your def saveStateInDB(state: State): Unit to def saveStateInDB(state: State): Future[Unit] If you want to suspend, you would write instead def saveStateInDB(state: State)(using Async): Unit Even if you tried to change erroneously to def saveStateInDB(state: State): Future[Unit] the type system would most likely catch you out, since you can't implement a Fourth, about structured concurrency: Yes that's built in. Finally, about function coloring: Capabilities are actually much better here than other language's proposals such as In summary: these are not your old style |
Beta Was this translation helpful? Give feedback.
-
The link for 《direct style scala》https://youtu.be/0Fm0y4K4YO8?si=O4F7Qs2bERdgplAQ |
Beta Was this translation helpful? Give feedback.
-
I am very curious what you think of rust's model, especially how tokio uses it, and how it compares to your suggestions. Futures are a type in rust that encapsulate executable code that is launched then polled for status by a scheduler. They can be passed around and stored, and until something that executes them like From my reading of your discussion I would suggest that yes, rust's use of async adds function coloring, but concurrent execution doesn't happen at all, and rust's borrow checker and thread-safe memory guarantees eliminate number 3 without it even being a core focus (in fact, not being able to share memory easily between async routines is a humongous bugbear in rust). The function coloring is annoying but with rare exceptions (closures, traits and recursion being big ones) pose limitations that need to be overcome; nothing is lost. I'd encourage you to read about it, as it might give you different perspective on how this may be tackled. |
Beta Was this translation helpful? Give feedback.
-
For reference: https://github.com/morganstanley/optimus-cirrus; for technical details https://skillsmatter.com/skillscasts/13108-monad-i-love-you-now-get-out-of-my-type-system is probably the best entry point. |
Beta Was this translation helpful? Give feedback.
-
Aka context receivers. Don't people find code like this weird?
I did read an intro to context receivers but code like this just baffles me. Why should a concept as simple as an implicitly passed parameter be mangled up like this? I would assume it's the same as (CoroutineScope ?=> R) which has a clear semantics (given in our Simplicitly paper). Why not do that? What's the point in warping the meaning of |
Beta Was this translation helpful? Give feedback.
-
@erikh It looks like tasks are closest things in Gears that correspond to Rust futures. A task is simply a lambda that takes an Async context and returns a Future. It can start running only when instantiated with an actual async context. |
Beta Was this translation helpful? Give feedback.
-
Yes that sounds very similar. In rust's case, tokio more or less ends up providing the context externally, and rust provides the language "stapling" for it to function by way of std::future::Future's trait definition.
…------- Original Message -------
On Monday, September 25th, 2023 at 8:00 AM, odersky ***@***.***> wrote:
***@***.***(https://github.com/erikh) It looks like tasks are closest things in Gears that correspond to Rust futures. A task is simply a lambda that takes an Async context and returns a Future. It can start running only when instantiated with an actual async context.
—
Reply to this email directly, [view it on GitHub](#19 (comment)), or [unsubscribe](https://github.com/notifications/unsubscribe-auth/AAAET22W5LHYE5PKZTJ5NSLX4GMAHANCNFSM6AAAAAA5DQET7I).
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
Because you can then call the methods defined on CoroutineScope inside the lamda, with scala you have to import the methods into scope. |
Beta Was this translation helpful? Give feedback.
-
Just put my two cents here: with JDk 21,I think the async and sync is blurry. And in future releases, the virtual thread's carrier thread may be ad hoc user defined. So what I would like to see is gears as a high performance async runtime, with that, I expect no Future or Async both in method parameter list and return type With the Caparese I think this can be done. |
Beta Was this translation helpful? Give feedback.
-
quoting @deanwampler here:
|
Beta Was this translation helpful? Give feedback.
-
I'm unsure if anybody mentioned this yet, and I don't know where to open this discussion, but
Future
and async/await aren't a good programming model, and I hope that, for whatever new effect system gets developed for Scala, this model gets left behind.I'm adapting here the sample based on this project's README:
Equivalent TypeScript/JavaScript code:
This model is the async/await pattern. It was implemented most prominently in C# and TypeScript/JavaScript. And it's error-prone, despite its popularity. The reasons are:
The problem is that function calls require the
await
keyword (or.value
in the Scala sample). Its absence leads to concurrent execution, and this is counterintuitive, always. In the sample above, the 2 methods could be using the same database connection, and that connection may not be safe for concurrent use.To make matters worse, "coloring" in this case isn't very type-safe if we don't care about the returned value. For example, consider that we have this function doing blocking I/O:
And we change it to an async one, changing its color:
In our code, the compiler will not warn us at the call-site when this change happens. At best, it may warn us that an unused
Future[Unit]
value gets ignored, but that's an unreliable warning in my experience. So code like this actually changes behavior in a dangerous way, as the call-site becomes "fire and forget":saveStateInDB(state) //...
Kotlin's Coroutines
Compare with Kotlin's Coroutines, which should be the current benchmark for UX, instead of the old async/await:
Note, there is no
await
keyword. That code looks just like blocking I/O code from Java. Except for thesuspend
keyword, which introduces "function coloring", except that this is actually useful, because going from this:To this:
Will make this code break if it's not in a coroutine scope, or otherwise the behavior of the program doesn't change. In other words, the compiler goes to greater lengths to ensure that the "color" matches, in a way that you can't do it with plain types:
And executing stuff asynchronously requires extra effort:
This function does more than meets the eye, because Kotlin also introduced "structured concurrency". In this case, we have 2 async tasks, and to ensure that no resources get leaked:
handle()
function cannot return with a result or an exception until all async children complete, even if we don'tawait
their result (e.g., usinglaunch
instead ofasync
from Kotlin's library).For a good introduction, see: Roman Elizarov — Structured concurrency (YouTube).
Java's Virtual Threads
Kotlin's Coroutines are very similar in spirit to Java's Project Loom (JEP 425).
Java prefered to return to blocking I/O, because it's a language that was built for blocking I/O (e.g., checked exceptions, try-with-resources, etc.). And in JEP 428: Structured Concurrency they are even introducing utilities for the "structured concurrency" approach (with more boilerplate, but does the job):
Again, notice how
findUser()
andfetchOrder()
are blocking functions, and if we want to execute them in parallel, we need extra effort. And while aFuture
data type is used here, that's only for awaiting on it in the same function, otherwise its use is completely encapsulated in the implementation. And if one of the tasks fails, theStructuredTaskScope.ShutdownOnFailure
ensures that the other task gets cancelled, and that thehandle()
function doesn't return until all tasks finish or get cancelled.Scala's Cats-Effect
Even in Scala, we actually have all the benefits from above via our current monadic effect systems, in addition to tracking side-effects:
No concurrent execution here, no threads got hurt in the process. This separation between expressions and their interpretation (referential transparency) is the main benefit of monadic effects. Note that you can get some of those benefits back, in normal Java code, by just working with blocking I/O, because here the real problem has been
Future
all along, and not necessarily imperative programming 😉If we wanted concurrent execution, the direct translation is error-prone, due to
start
being a "primitive":This is error-prone because the parent fiber doesn't wait for its children. I hear that ZIO does that, but haven't checked. This means that if one of the fibers ends in error, the other doesn't get cancelled automatically. But
start
is known to be low-level, so one might specify parallelism explicitly:Or, a closer translation to the "structured concurrency" approach is to use Resource and background. This ensures that if one fiber fails, the other gets cancelled:
Others
A popular blog post that advocates for "structured concurrency" is: Notes on structured concurrency, or: Go statement considered harmful.
What's interesting about it is that it attacks the very existence of a
go
statement in the Go programming language:This starts a "goroutine", meaning that the function gets executed asynchronously. For our purposes, this is like
Future.apply
in Scala,scope.fork
in Java's JEP 428, orlaunch
in Kotlin's coroutines. It's considered "harmful" because it's "fire and forget", which leads to leaked resources and complexity, the author comparing it to the GOTO statement.Interestingly, the Go programming language does not have an
await
keyword, even if it does M:N scheduling. Alongside Kotlin, it's one of the first languages to go that route.TLDR — I hope that Scala does not implement
async/await
and moves away fromFuture
, because the industry is moving away from it.Beta Was this translation helpful? Give feedback.
All reactions