Hacker Read top | best | new | newcomments | leaders | about | bookmarklet login

I remain unconvinced that red/blue functions are a good thing in a statically compiled language.

If anything, the amount that async/await spirals outward to other areas in Rust demonstrates that it doesn't seem to be a well contained feature.

In addition, async/await executors in Rust quite often fail on corner cases. If I want to only wait on a socket, stuff works great because that's what everybody wants.

If, however, I want to do something like wait on sockets and wait on a character device file descriptor ... that tends to break in fairly mysterious ways.



sort by: page size:

While I agree with his points in principle, Rust isn't actually a great example to use.

Rust allows to simply spawn threads, right? So a function not being marked 'async' doesn't necessarily give any guarantees about potential side effects.

Spending most of my time writing Elm, having side-effects present in the type system is great. But colored functions in Rust (as well as most other async/await langs) only creates a devide between functions that provide a specific kind of asynchrony, and those which do not.

So async/await improves the syntax of writing a specific kind of async code, at the cost of composability (the red/blue divide). It does not, however, make any guarantees about the asynchronous nature of your code.


My take on this:

Red and blue functions, and async / await, are a good thing when implemented well. It's much better than implicit every function can be async (e.g. python's gevent). Because it 1) makes it clear what is and isn't async, and 2) lets the programmer convert async to a callback when necessary.

The real issue is that some languages have bad implementations of async / await. Specifically Rust, where afaik traits can't have async and that makes using async really convoluted. And most languages don't have async-polymorphism, and maybe other features I can't think of, which ultimately create the problem "I have to call an async function in a sync function without blocking". With a good implementation of async / await and half-decent architecture you shouldn't have that problem.


Quoting this article's opinion on the 'What colour is your function' article

> One the one hand it's a great article that hits the nail on the head why using async/await in JS (and other languages) is very painful

> Rust's async/await even though - you might have guessed it - it doesn't apply!

So the author starts off by declaring that the colored functions problem only applies to other languages but not to rust.

Soon after, he takes exception with the claim "You can only call a red function from within another red function".

His primary argument in support of this is that in rust you can execute an async function blockingly in an executor. And to do vice versa, i.e. call a sync function in an async loop, that you can either block the event loop or hand it off to a different machinery.

The fact is, both of these things can be done in other async runtimes as well. For eg. it's a near identical scenario in python async. Like others have pointed out in this comment thread, this is true in Scala too. Perhaps the author is unaware of this?

So yes, rust does indeed have the colored function dichotomy, as any async runtime would, the author's angry assertions to the contrary notwithstanding. (I say this as a fan of both rust as well as async runtimes, which I feel are a nice fit for certain use cases.)


Once again, the article is about JavaScript and everything it says still holds today, introducing async/await didn't change anything. Sync functions can only call other sync functions and use the result immediately. To use an async function, you have to convert the caller to an async function too which can be anything from annoying to impossible.

So yes, Rust still has colors, but it doesn’t matter because a red function can call a blue one without a problem and vice versa. You’re right in saying that async functions have a cost and shouldn’t be used indiscriminately - so just use them when it makes sense. As opposed to JavaScript, Rust doesn’t make you commit to one or the other early and either face major refactors in the future or pay the price of async when it’s not required.

P.S. I think there are some caveats for library authors and also to blocking the thread on a single future, but maybe more qualified people can comment on those.


It's sad that we still have to make async I/O explicit in the code to obtain some efficient concurrency in 2020.

Async/await is a huge improvement over callback hell, but this doesn't fix everything. The "function color" problem still exists [1], and seems to be more than binary in rust. This quote from the article is incredibly sad: "each async library, comes its own ecosystem of libraries, which only work with that async library".

Rust is ground breaking in some areas, but also completely lacks innovation in others.

But is it actually _necessary_ to resort to async I/O in Rust, given that the type system appears to make thread-based concurrency safe ?

[1] https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...


What's so wrong with Rust's async / await?

I don't agree with the article that the idea of colored functions is tied to dynamic types. I think it applies equally well to Dart, TypeScript, C#, and any other language considering async await. It's mostly a question of usability and the ability to encapsulate use of asynchronous operations.

But for Rust, I think having colored functions is probably the right call. Asynchronous operations are fundamentally different at the OS/machine level. Since Rust is a language where code deliberately has very high affinity to what the underlying system is doing, it makes sense to make that distinction visible to users even if it means that it's harder to write and reuse asynchronous code.


The async-std is taking the “always red” approach that the article mentions and that wasn’t possible until now due to async being hot of the presses. The rest of the arguments in the article are based on the point that “red functions are more clumsy to call” which doesn’t hold for Rust, but holds for JavaScript.

Yeah, waiting for Rust's async/await to mature is probably a good idea. But I'm not sure why they didn't just use threaded I/O instead.

The post makes a good point, albeit in a long-winded way.

My short summary would be in languages that result in function-coloring due to async/await, using Future/Promise directly avoids the coloring and the ecosystem is better off.

Places where async/await are ok are JS (because both the browser and node/deno) are for the most part in an async context, and Go since everything's a goroutine hence all one color.

Languages such as Java have worked well with Future/CompletionStage and doesn't really need the convenience of writing async/await which translates the rest of the program after await into a hidden 'rest of program' callback. That async/await syntactic convenience is not worth the complexities added to libraries. Rust including async/await was a poor 'cargo cult' choice. I would welcome a Rust fork/ecosystem without it.


I think you misunderstand my point slightly. Here’s the difference: in JS everything that needs to be async is actually marked async. So when you use a browser api or library function you know for certain. I agree I like that and want it for Rust. In Rust the standard lib is not async aware so it cant be used safely from async code, period. And tons of Rust code uses the stdlib and it’s not super practical to just not use it so you’re stuck asking whether something is safe to call from async code or not with zero help from the compiler. So we agree not having the compiler do this sucks.

(What I meant by partially skirting the colored function issue is that you don't have to call await on the result of a function if you dint want to. So you can call a blue function from a red function just fine and do stuff with the result. You just can’t await it. This adds back lots of flexibility that some async impls in other languages don’t allow for.)


Hear, hear

The worst part of Rust, too, is that async/await is tacked on— and it’s just a bad primitive to start with.

I have many more thoughts on this that I might like to blog about, but it can be improved by having a async runtime of some kind (we don’t call malloc a runtime, but it is— and you need something similar for ergonomic async) — to wit, the de facto pseudo-answer for this in Rust is tokio (which should just be promoted to the stdlib, at this point; the vast majority of async code depends on it’s particularities).

It’s also causes quote-unquote “function coloring problems” left, right, and center — which is also avoidable. Just for kicks, consider a language with “call-async”/“yield”. Tie that in with a runtime and this whole topic becomes much, much, cleaner. Async at the callsite! (This is the blog I’ve been meaning to write)


Thank god. Async/await is an okay tradeoff for Rust, which lacks a managed runtime, but it is simply a bad abstraction for managed languages, where the runtime can automagically do the right thing for you without user involvement.

That's a Javascript rant from 2015, and almost completely unapplicable to Rust. The rust designers were well aware of the disadvantages of various async implementations and have learned some lessons.

First he starts with a rant about the color of a function. In that sense, all typed languages have a color, aka the type of their return parameters. Sure, there a advantages to dynamic languages, but they're going out of fashion now for some good reasons.

Then he lists 5 more specific drawbacks of async code in 2015 Javascript 2015. All 5 aren't issues in Rust.

#1. Every function has a color: that's how typed languages work

#2. The way you call a function depends on its color: the rust compiler doesn't let you get this wrong and provides cut and paste corrections when you do

#3. You can only call a red function from within another red function: Use Future#poll

#4. Red functions are more painful to call: This is in reference to callback syntax in old node.js code and is fixed by the await sugar even in modern Javascript

#5. Some core library functions are red: The clause "that we are unable to write ourselves" is untrue in Rust; you should never have a need to drop down into C to write anything. And this is really just a variant of #3; if it's trivial to convert async to sync this wouldn't be an issue.


It’s not just that. There’s a few big problems with Rust async aside from the normal coloring problem that is inherent to async and not worth talking about.

* The async runtime and async functions are decomposed but tightly coupled. That means while you could swap out runtimes, a crate built against one runtime can’t generally be used with another unless explicitly designed to support multiple. I believe C++ has a similar problem but no other major language I’m aware of has this problem - that’s typically because there’s only a single runtime and it’s embedded in the language. Things like timers and I/O are not interoperable because the API you use has to be for the runtime you’re running under. I believe there’s work ongoing to try to remedy this although in practice I think that’s difficult (eg what if you’re using a crate relying on epoll-based APIs but the runtime is io_uring).

* async in traits. I believe that’s coming this year although the extra boxing it forces to make that work makes that not something 0-cost you can adopt in a super hot path.

* async requires pinned types which makes things very complex to manage and is a uniquely Rust concept (in fact I read a conceptually better alternative proposal for how to have solved the pin/unpin problem on HN not too long ago, but that ship has long sailed I fear).

* The borrow checker doesn’t know if your async function is running on a work stealing runtime or not which means there’s a lot more hoop jumping via unsafe if you want optimal performance.

* async functions are lazy and require polling before they do anything. That can be a bit surprising and require weird patterns.

Don’t get me wrong. The effing-mad crate is a fantastic demonstration of the power of algebraic effects to comprehensively solve coloring issues (async, failability, etc). But I think there’s stuff with runtime interop that’s also important. I don’t think anyone is yet seriously tackling improving the borrow checker for thread per core async.


I think Rust’s async stuff is a little half baked now but I have hope that it will be improved as time goes on.

In the mean time it is a little annoying to use, but I don’t mind designing against it by default. I feel less architecturally constrained if more syntactically constrained.


The article explicitly admits that async/await is ergonomically much nicer than explicit futures/promises. But the color problem still remains, one consequence of which is duplication of code and interfaces.

Arguing that the problem doesn't exist if you only stick to functions of a single color isn't a rebuttal, it's an admission! But the fact of the matter is async functions have real limitations and costs, which is why they're not the default in Rust, which in turn is why any Rust program will always have some mix of differently colored functions. But, yeah, the fewer of one color and the more of the other color, the better. That's the point.


The last thing I would take away from async/await in Rust is that it's "half baked." It's incredibly deeply thought out with years of RFCs, great contribution work that required both low level implementations in nightly and creating extensions to the memory model, and extensive bike shedding and discussion with the community on surface APIs.

I've been using C# and (Java|Type)Script pretty much since they were invented. Both use a function coloring async system. I don't know what it's like in Rust, but at least in the two examples with which I have experience with this apparently much-maligned system, I really don't get the complaints. Having to "color" functions really isn't that big of a deal.
next

Legal | privacy