News for hackers. Stuff that matters.
Re:The Rust I wanted had no future by sph on Monday June 05, 2023 @04:40AM (#36193895)
I still disagree a little. You can call an async function in Rust from a regular one, that's true, but the returned value still needs to be passed through an executor to be useful.

Async without function coloring means being able to call `async add(a, b: usize) -> usize` and having an usize back whether you're calling from a regular or async function. If you need slightly different logic because of async, it is indeed function coloring.

In fact, a hypothetical async without coloring would not even need the `async` keyword at all, as all function would be effectively the same so could be called asynchronously or not. I'm not sure that is even possible with this abstraction.

Re:The Rust I wanted had no future by creatonez on Monday June 05, 2023 @04:53AM (#36193974)
Thing is, it's a function coloring that mostly doesn't impede you by making certain operations impossible without massive refactoring. Regardless of an extra method call needed, it gets rid of the "you can only call a red function from within another red function" barrier by allowing you to do it, as long as you specify how (execute inside this thread, or delegate to a worker thread). This makes most of the issues associated with Javascript function coloring evaporate.
Re:The Rust I wanted had no future by sph on Monday June 05, 2023 @05:00AM (#36194014)
Fair enough. It's not a deal-breaker kind of coloring, my issue is mostly conceptual. It's simply not elegant as a construct, but perhaps it doesn't have to be.
Re:The Rust I wanted had no future by johnnyanmac on Saturday June 10, 2023 @07:22AM (#36269562)
My experience with asynchronous programming is brief, but it taught me that you will never have elegant parallelism. It's a completely different problemset, you need a completely different solution to handle it. But the ideal API wants to treat the problems as one in the same.

So the two camps I've seen on solving it is either

a) blackbox it and set some rules for implementers, inevitably leaving to some hidden and very nasty bugs on the user end or bottlnecks for advanced users, or

b) give implementers the full tools to setup, launch, and synchronize themselves, which may end up being slower than a single threaded solution if the user isn't adept already with parallel programming.

You inevitably always have bits of a) creep in, even for the most explicit solutions

Re:The Rust I wanted had no future by ohgodplsno on Monday June 05, 2023 @05:12AM (#36194075)
Not knowing about function coloring is a stupid dream and has terrible implications on the performance of your code, which is infinitely more important than you losing 2 minutes having to figure out that you really want to `runBlocking { callThatBlocksForADamnLongTime() }`.

The insight that function coloring gives you is terribly important. Roman Elizarov (of Jetbrains, author of Kotlin and lead on Kotlin Coroutines) agrees too: https://elizarov.medium.com/how-do-you-color-your-functions-...

async functions without coloring means that the only warning you'll ever get that `calculate2Plus2()` actually ends up running a distributed BigQuery and writing the end result to disk, printing it to stdout() and parsing that result to give it back to you is... hopefully, documentation is up to date and you read it?

Async function coloring is not a problem. Async function coloring is a solution to "software developers are awful and will do awful things without any warning". If `calculate2Plus2` did not exhibit the write to disk behaviour in v1.0.0, but bumping to v1.0.1 does, I'd really want a warning that it does at least, and ideally a compiler error. The proper solution to function coloring is to have a pleasant API to interop between both worlds so that, at worst it's just a dozen characters more to say "yep, I really want to block here".

Re:The Rust I wanted had no future by mike_hearn on Monday June 05, 2023 @05:31AM (#36194170)
Async doesn't solve that problem. What you're asking for is some sort of expression in the type system of expected performance, but there's no tech that can do this. Consider that modern NVMe SSDs can do disk reads faster than many calculations over the content of what was just read. A function that changes its algorithm to have worse time complexity won't be marked as async but could break your app, adding a single disk read would require marking as async but probably won't break your app (especially if it's in a non-hot path), adding 10,000 more reads where previously you had one doesn't require changing your async annotation but might break your app, etc.

So the whole sync/async distinction doesn't really make much sense outside the context of single threaded UI toolkits, where indeed you have to read the docs anyway because doing some very CPU intensive work on the UI thread will block it even if no "blocking" APIs are ever invoked (and what is or is not blocking is somewhat arbitrary anyway in many cases).

Re:The Rust I wanted had no future by hayley-patton on Monday June 05, 2023 @06:09AM (#36194389)
There are type systems which can track complexity (e.g. http://gallium.inria.fr/~fpottier/slides/fpottier-2011-01-jf...) but that's still different to performance; async/sync tells us things like "touches disk" or "touches network" but not "O(2^n) might take a really long time".

Alternately, an effect system can communicate "touches disk", can make functions which are polymorphic to effects (e.g. map touches the disk if the mapping function touches the disk), and can distinguish different effects, unlike async/sync being anything-or-nothing. IMO async is more of an implementation detail (as "may suspend/yield" is not a very precise or interesting effect), whereas effects would communicate the properties that ohgodplsno wants to know.

Re:The Rust I wanted had no future by mike_hearn on Monday June 05, 2023 @06:33AM (#36194549)
Yes, but are there any effects systems in use outside of maybe Haskell? Effects seem mostly to be stuck in the research lab and have been for a long time.
Re:The Rust I wanted had no future by hayley-patton on Monday June 05, 2023 @06:37AM (#36194566)
I don't mean "effects" in the sense where you can have effect handlers which get delimited continuations and all; it'd just mark what could happen (like checked exceptions). But I can't think of any languages with that and not effect handlers; Koka, Eff, and Unison come to mind for effects, though their practical-ness may vary.
Re:The Rust I wanted had no future by zozbot234 on Monday June 05, 2023 @07:09AM (#36194761)
Do you really need effect handlers as a separate feature once you have async/await? It seems like you could easily encode one with the other.
Re:The Rust I wanted had no future by hayley-patton on Monday June 05, 2023 @09:48AM (#36196159)
An effect handler is allowed to invoke a continuation multiple times; I believe this is not possible with async/await.
Re:The Rust I wanted had no future by ohgodplsno on Monday June 05, 2023 @07:13AM (#36194792)
Verse has effects, which by virtue of exposing it to the entire Fortnite community probably makes it a bigger language than Haskell in terms of user counts.

https://dev.epicgames.com/documentation/en-us/uefn/specifier...

converges/diverges, computes, varies, reads, writes, transacts, no_rollback.

Re:The Rust I wanted had no future by ohgodplsno on Monday June 05, 2023 @06:21AM (#36194481)
Adding a disk read won't break my software, but it can certainly make my software non-portable. If it suddenly expects a disk write and I'm running on a ROM, I'm going to have a hard time. If it suddenly tries to contact google.com and I'm on an airgapped machine, I'm going to have a hard time. It's not even a matter of breaking the app actually, it's a matter of warning that "I'm going to go outside the realm of just your CPU and your memory". Sure, you could also mark as as async a function that adds two numbers. It's stupid, it's a JS thing to do, but you could. Except that you need to actively opt-in to being async in this case, as opposed to have it be forced upon you because you used a function that is also async.

Blocking has meaning in a lot more contexts, and being a consultant on JVM related topics, you should know that: it's the entire purpose of Project Loom, and Loom doesn't entirely get rid of colour coding either explicitly for that purpose. Loom wasn't made because the guys at Oracle have a deep love for JavaFX, but rather takes into account the server world, where you really want to know that you're going into another context, another computer, etc. The only time where the existence of async doesn't make sense is if your entire language and ecosystem expects everything to already be distributed. In which case, you've just switched the default color to async.

Finally, you chose to read async as the current JS/C# abomination implementation, but most of the sensible languages have implemented it as an effect: Kotlin has suspend funs, they don't return a Promise, but they tell you two things: they're going to touch something like the disk or the network, and if you really want to have them in a non-suspend context, you can either get a Deferred<T> out of them (and find another thread to run it on, and handle synchronization yourself), or run them on the current thread (and block everything).

Re:The Rust I wanted had no future by mike_hearn on Monday June 05, 2023 @06:31AM (#36194539)
Blocking in Loom is primarily about waiting for network traffic, possibly with extensions to file IO in future - it doesn't suspend if you do file IO today. Loom does get rid of coloring, or rather, doesn't introduce more of it and lets you phase out what exists, so I'm not sure what exactly you mean by that.

Kotlin suspend funs do not tell you they're about to touch network disk, that's the reason they use "suspend" and not "async". Suspend funs can also use generators (with yield) in which case no I/O is happening but they are nonetheless suspending.

So this is why blocking as a concept isn't a great one, IMO. The Kotlin designers were right to not use the word in their implementation of coroutines. Where Loom discusses blocking a bit, it's not defined as being about blocking, it's about being able to scale up threads to way beyond what was previously possible. It just so happens that the primary reason you'd want to do that is if you have lots of threads that spend lots of time waiting for things, but that doesn't automatically require network or disk access. For example you can use threads that spend all their time in Thread.sleep if you were writing an agent simulation.

Re:The Rust I wanted had no future by kaba0 on Monday June 05, 2023 @09:59AM (#36196259)
> possibly with extensions to file IO in future - it doesn't suspend if you do file IO today

Wasn’t basically all of the JDK’s file APIs rewritten to io_uring-like calls to support Loom? I have thought that IO was definitely something that Loom handled.

Re:The Rust I wanted had no future by mike_hearn on Monday June 05, 2023 @01:05PM (#36199390)
Last I heard it was on the todo list, but maybe it got done since I last looked. You can do async IO without io_uring.
Re:The Rust I wanted had no future by coldtea on Monday June 05, 2023 @09:04AM (#36195620)
>Adding a disk read won't break my software, but it can certainly make my software non-portable. If it suddenly expects a disk write and I'm running on a ROM, I'm going to have a hard time. If it suddenly tries to contact google.com and I'm on an airgapped machine, I'm going to have a hard time. It's not even a matter of breaking the app actually, it's a matter of warning that "I'm going to go outside the realm of just your CPU and your memory". Sure, you could also mark as as async a function that adds two numbers. It's stupid, it's a JS thing to do, but you could.

You're conflating too many orthogonal things.

It's not merely the performance of touches disk/network that async was used for those, it's because that waiting is not because you're held up by the language doing calculations. That isn't the case with a function like you describe.

Marking functions async when they aren't yielding just to signal that they might be slow is a bizzare idea. That's not what async is and it doesn't bring any real benefit, it's just abusing the notion (and limiting the contexts where you can run those functions). You'll still be using libraries which wont follow this strange idea, and you should know their performance characteristics.

Blocking code exists in all major languages, including JS. In a single thread context, having something "async" wont help you at all, if it calls anything blocking, which can be something as common as JSON.parse

>If it suddenly expects a disk write and I'm running on a ROM, I'm going to have a hard time. If it suddenly tries to contact google.com and I'm on an airgapped machine, I'm going to have a hard time.

All of those have nothing to do with async, and what async is created and used for.

What you want is something like Haskell's IO "tainting", a (side) effects system, or something to that (no pun intended) effect.

Re:The Rust I wanted had no future by lmm on Monday June 05, 2023 @07:34PM (#36205873)
This is a perfect solution fallacy. Yes, "whether this function will suspend (and shift execution onto a different thread)" does not tell you absolutely everything about that function. But it tells you things that are worth knowing, that you want to be visible (not super intrusive, but visible) in your IDE. And in a language with a decent type system the cost is pretty small; functions that are agnostic about whether they will run as async can and should simply be polymorphic in async-ness.
Re:The Rust I wanted had no future by anonymoushn on Monday June 05, 2023 @08:31AM (#36195315)
This comment reads like it's from an alternate dimension where "calling read(2) marks functions as async" is a common language feature. Over here, read(2) can block forever and normal functions can call it in just about every language.
Re:The Rust I wanted had no future by belmont_sup on Tuesday June 06, 2023 @10:29AM (#36213348)
I agree that hiding coloring entirely is really a trap. I want to know when some IO work runs because it means potential exceptions unknown with unpredictable latency in my program.

As a design choice of OCaml’s eio - an async runtime, you must pass around a dependency to all your IO functions. With the “net” object passed visible in my function type signature, I can tell some network work may occur. The first benefit is we get synchronous Go-like code.

Re:The Rust I wanted had no future by kaba0 on Monday June 05, 2023 @05:13AM (#36194082)
Virtual threads (like java’s loom, erlang’s and goroutines) are arguably that.

In case of java there really is no compiler magic even, “just” runtime magic.

Re:The Rust I wanted had no future by pas on Monday June 05, 2023 @04:43PM (#36203318)
how big is the overhead for this? and how much easier is working with virtual threads than Rust's async?
Re:The Rust I wanted had no future by winrid on Monday June 05, 2023 @05:34PM (#36204081)
1. Low enough that you don't need to worry for most web backend code and 2. Anything is easier than that. *XYZ is not Send* amiright?

With Rust's async you have to worry about a different worker thread picking up the work after a context switch, which makes things complicated. Not so with Java.

Re:The Rust I wanted had no future by josephg on Monday June 05, 2023 @06:09AM (#36194394)
Its certainly possible with compiler magic. Zig does exactly that - where functions can be colorless and they work slightly differently in async and syncronous contexts, doing the obvious thing in both cases. I'm not sure how well it works in practice (I haven't spent enough time with zig to know). I'd worry that bugs would creep in to whichever variant of the function you aren't exercising.

Its a lovely design though.

https://kristoff.it/blog/zig-colorblind-async-await/

Re:The Rust I wanted had no future by svieira on Monday June 05, 2023 @11:24AM (#36197571)
_Unyielding_ describes the issues that arise when the author of the code doesn't have control of the transactionality / atomicity of their operations at the level they need to: https://glyph.twistedmatrix.com/2014/02/unyielding.html

To my mind it's very much a balancing act between "low power to the developer, high power to the language" and "high power to the developer, low power to the language" all up and down the stack from software / hardware to "consumer / framework".

Re:The Rust I wanted had no future by svieira on Monday June 05, 2023 @05:35PM (#36204104)
See also: https://www.tedinski.com/2018/11/06/concurrency-models.html which is the first article I've ever seen that says that `m:n`-style-green-threads are bad and good at the same time.

TL;DR - he says that "doParallel" and "doConcurrently" are separate operations with distinct semantics that designers of programs must care about and that conflating the two (especially the common "doConcurrently-and-often-but-not-always-in-parallel") is one of the most common causes of bugs in programs that need to make progress in multiple threads of execution.

Re:The Rust I wanted had no future by astrange on Monday June 05, 2023 @09:24PM (#36206867)
A notable and common thing about this article is that it confidently declares futures/promises are good for parallelism, but this is wrong - they are flawed because they introduce priority inversions.

To schedule tasks properly, you need to know who is waiting on them as early as possible. With a promise, you only know when you get to the "wait()" call, which is too late.

The correct solution is called structured concurrency.

Re:The Rust I wanted had no future by svieira on Tuesday June 06, 2023 @02:20PM (#36216978)
Depends on if you're using Monix/Haskell style tasks (lazy, run when depended on) or JS-style promises (E Promise / C# Task / Java Future - run when instantiated, waiting is post-submission). Agreed completely though, structured concurrency is fantastic.
Re:The Rust I wanted had no future by xmcqdpt2 on Monday June 05, 2023 @07:19AM (#36194829)
You can do it using a runtime with scheduling and fibers.

The issue is that you want to be able to save the entire thread stack cheaply so that you can switch to another task at an async yield point, and then go back to your previous stack frame. You want to do this without spawning system threads.

Say you have an f -> g -> h call stack that blocks on IO. If all the functions are in fact async state machines (or some other kind of callbacks) your thread stack will look like this:

Executor.loop() -> Executor.run(h) -> h.await()

That is, your functions are really objects that are queued up and taken as needed by the executor. If h has a yield point, you can put it back on the queue, go back to the loop and pick up some other work. Later you can do the above again and await h to again up to the next yield point.

Now consider the case where the middle function g is NOT async. In that case the call from f to g will be a normal function call that gets put on your call stack. In a language where this is allowed, you'll still have to wait or execute h and you'll get a call stack that looks like,

Executor.loop() -> Executor.run(f) -> f.await() -> g() -> h.awaitUntilCompletion()

(where our awaitUntilCompletion is minimally just calling h.await in a loop until h is finished). At this point, you are stuck: you can't yield from g() because it is a normal function with a normal stack frame, so your thread has to wait till g() finishes before it can be used for anything else. You are basically blocking a thread on h. At this point you might spawn a new thread to keep your thread pool count up (which is expensive and memory hungry) or just accept the forced synchronous blocking (which reduces throughput).

If this happens over and over again, you either end up running out of memory or end up running code in a synchronous fashion but with worse performance (because of all the state machines). This is why functions are usually coloured, so that you don't get yourself in this accursed state by accident. AFAIK the alternative is to allocate all of your call stacks in the heap so that you can switch them in and out of kernel threads, which is what fibers are. This requires a complicated runtime with a scheduler such as in Loom,

https://cr.openjdk.org/~rpressler/loom/Loom-Proposal.html

Re:The Rust I wanted had no future by belmont_sup on Monday June 05, 2023 @07:54AM (#36195040)
Algebraic effects like how OCaml’s now implementing it may be possible! The ecosystem around effects in OCaml is still really young, but here’s an example of an http request being made that is asynchronous, non-blocking but looks synchronous with no special syntax.

https://github.com/mirage/ocaml-cohttp/blob/16e991ec1f7e5f0c...

Performing these effects is similar to throwing exceptions up the callstack where whichever ancestor handles the IO work, then resumes the child with IO work done in hand.

Re:The Rust I wanted had no future by mjmahone17 on Monday June 05, 2023 @08:50AM (#36195483)
As someone who likes OCaml, but hasn’t touched OCaml in ~5 years, that’s a very hard example to read. I can tell it’s making a network request, but I have no clue:

- what the type of res is (is it a string? A buffer? Async string? Something else?)

- how this code does not block: what is happening in parallel? At which point will it block?

I would like to be excited about OCaml’s algebraic effects, but right now I don’t really get it.

Re:The Rust I wanted had no future by yawaramin on Monday June 05, 2023 @11:29AM (#36197675)
- It's some type from which you can read a response body. In OCaml we often work in terms of abstract types i.e. the operations which can be done on types. If you want to see the exact name of the type you can always look up the signature of `Client.get`: https://github.com/mirage/ocaml-cohttp/blob/16e991ec1f7e5f0c... (nowadays LSP support in any good editor will show the type on hover)

- It's implementing non-blocking I/O using effect handlers. The complexity is not exposed to the library user, which is actually the whole point. If you want to dive into the concurrency library (Eio) and study how it works, that's very doable, just like any concurrency library in any other language.

There's really not that much to get–OCaml will do async I/O without having to colour functions, just like Go, Erlang/Elixir, and soon Java. It's not something sexy like monads that excite people with mysterious FP concepts, it's just a lot of hard work that went into simplifying how developers code at scale.

Re:The Rust I wanted had no future by belmont_sup on Tuesday June 06, 2023 @10:21AM (#36213233)
Yawar’s explained it all, but I’ll share tidbits of my own.

When reading ocaml on _text_ like from GitHub, I personally accept that I won’t need to know the type and underlying data structures just yet unless I read and have open the .mli file.

I’m excited first and foremost because we can have async code feel synchronous like Go.

How does the code run? IO runtime is defined in userland, like Rust’s Tokio. That’s the “Eio_main.run” part. Underneath, eio spins up and manages processes in a non blocking manner, even scheduling work.

Since effects are essentially resumable exceptions, this particular http request throws an effect to the runtime. The logic and this particular process pauses (okay I may be wrong here). Logic flow now continues somewhere in our runtime level (the effect handler), where all the async IO work happens. The runtime resumes the child process and returns the http result back to the child via a “delimited continuation”.