Concurrency's correct primitive is Hoare's Communicating Sequential Processes mapped onto green threads. Some languages that have it right are Java (since JDK17 - Java Virtual Threads), Go, Kotlin.
Always with the blooming red and blue functions. You can say exactly the same thing about const.
The fact that a function can perform asynchronous operations matters to me and I want it reflected in the type system. I want to design my system on such a way that the asynchronous parts are kept where they belong, and I want the type system's help in doing that. "May perform asynchronous operations" is a property a calling function inherits from its callee and it is correctly modelled as such. I don't want to call functions that I don't know this about.
Now you can make an argument that you don't want to design your code this way and that's great if you have another way to think about it all that leads to code that can be maintained and reasoned about equally well (or more so). But calling the classes of functions red and blue and pretending the distinction has no more meaning than that is not such an argument. It's empty nonsense.
> The fact that a function can perform asynchronous operations matters to me and I want it reflected in the type system.
async doesn't tell you whether the function performs asynchronous operations, despite the name. async is an implementation detail about how the function must be invoked.
As TFA correctly points out, there's nothing stopping you from calling a blocking function inside a future, and blocking the whole runtime thread.
I didn't say it tells me whether the function does perform such operations, I said it tells me it can. More importantly it tells me which functions (most) can't.
It would be swell if functions could be generic over this capability at compile time, so that you could get the same guarantees from the type system without implementing the same protocols more than one time.
An async function is some syntactic sugar around a sync function that returns a future. You can merrily call one from the other.
You can only convert an int to a float with significant caveats. It's not a general trivial conversion. More complicated types may not be convertible at all or behave in all sorts of exciting ways (including having arbitrary side effects).
The point is that none of that is different to async functions. Of course you have to know what to do with them for them to be useful, but there is no requirement for them to "infect" calling code.
Virtual threading is fun and all until you find out SimpleDateFormat and a bunch of other classes built tight into your standard library aren't thread safe and now you need to go through your program and find out what else you missed. Go too has these fancy green threads at the cost of manually locking resources and finding out about race conditions when you forget about them.
Futures aren't a fundamental CS mistake, they're a design decision. You may disagree with that decision, but the advantage Rust brings is that you don't need to worry about thread safety once your program actually compiles, at the cost of different code styles.
Neither asynchronous processing design is fundamentally wrong, they both have their strengths and weaknesses.
> Virtual threading is fun and all until you find out SimpleDateFormat and a bunch of other classes built tight into your standard library aren't thread safe and now you need to go through your program and find out what else you missed. Go too has these fancy green threads at the cost of manually locking resources and finding out about race conditions when you forget about them.
Why would that ever be an issue? Instances of those classes shouldn't be shared between virtual threads just the same as when using regular threads.
I want colored functions. I want to know which code is running synchronously and which doesn't, which raises errors and which doesn't. Color is just a description of the function's properties (and effects) and how it's compatible with other colors.
There is also nothing fundamentally bad with cooperative scheduling in scope of a single process.
I got into programming in the 1990s. At that point in time, there was still a large contingent of programmers loudly insisting they needed assembly language to do everything. And to be clear, I mean, everything. Not "Yeah, I can't really bring up an OS without a bit of specialized assembly" but "every programmer should write every program in assembly".
The vast majority of them were already wrong. They only got more wrong.
You may just be used to knowing what code is "synchronous" and what isn't because it's been shoved into your face and you've adapted your thought process to it. In practice, "everything important is doing something 'asynchronously'" turns out to be the vast majority of what you need, and the vast majority of your mental energy you are dedicated to splitting the world in two is a waste. For the little bit that remains, by all means use something specialized, but it's just not something that everyone, everywhere, needs to be doing all the time, any more than everyone everywhere should be manually allocating registers, or any more than programs need to have line numbers because otherwise how can they work? (One of my favorites because I remember having that conception myself.)
I think you have your analogy backwards: the "assembly programmer" in this situation is the person who doesn't understand why one would "color" functions and/or express a fundamental property as part of their types. "Why do we need to express this in their type? Every programmer should be able to understand this without help".
> Without function coloring there's no distinction that you need to know of
Why do you think you don't need to know of it? I want to know if the function I'm calling is going to make a network request. Just because I can have a programming language that hides that distinction from me doesn't mean I want that.
Ideally I want to have the fundamental behavior of any function I call encoded in the function signature. So if it's async, I know it's going to reach out to some external system for some period of time.
getaddrinfo() is a synchronous function that can do network requests to resolve DNS. The network property isn't reflected in its function signature becoming async. You can have an async_getaddrinfo() which does, but the former is just a practical example of network calls in particular being unrelated to function coloring.
It's nonsense. Async in rust is just syntactic sugar around a function signature. You can merrily call async functions from sync rust, you just have to know what to do with the future you get back.
"you just have to know what to do" is the problem. You can call any color from any color, but for some colors it's trivial, e.g. sync function from a sync or async one, or a non-failing function from a failing or non-failing one.
I don't want to be able to call fallible function from an infallible one trivially, I want the compiler to force me to specify what exactly I'm going to do with an error if it happens. Likewise for async-from-sync: there are many ways I could call these: I can either create a single threaded executor and use it to complete the future to completion, or maybe I want to create a multithreaded executor, or maybe I expect the future complete in a single poll and never suspend and I don't even need a scheduler.
Well yes to all that. I still don't see the problem. An async function isn't really an async function, it's a sync function that returns a future. Would it be better if all that was manual? I've done quite a bit of stuff using manual async traits and it's painful and I highly value the syntax sugar that async brings. That said, I certainly don't want some executor running quietly behind the scenes doing async stuff for me without my explicit and full control. If I want to manually poll a future, that's for me to decide.
You seem to raise valid points and I don't disagree with you, however I don't see how it's relevant to the original concern regarding colored functions.
I suppose I'm struggling to understand what "colour" means in the context of Rust. It's surely just another word for signature. For some reason it's trotted out every time there's a discussion about async. I can only assume it's to do with the original use of the term for JavaScript async (which I know almost nothing about and have no opinion on), but I just cannot see its point in Rust async.
It has to do with the fact that most of the code in the project is not async but having to call async functions often propagates all the way to your main function. It's infectious and many people don't like it, myself included, that's why I'm working with Elixir and Golang where async is transparent and 99% automatic, or explicit but non-infectious, respectively.
I do love Rust and found a number of very valid uses for it but its async story leaves a lot to be desired. I don't enjoy writing it though I do enjoy the results.
1. inability to read an async result from a sync function, which is a legitimately major architectural limitation.
2. author's opinion how function syntax should look like (fully implicit, hiding how the functions are run).
And from this there is the endless confusion and drama.
The problem 1 is mostly limited to JS. Languages that have threads can "change colour" of their functions at will, so they don't suffer from the dramatic problem described in the article.
But people see languages don't fit the opinion 2, of having magic implicit syntax, and treat it as an equally big deal the dead-end problem 1. But two syntaxes are somewhere between minor inconvenience to actual feature. In systems programming it's very important which type of locks you use, so you really need to know what runs async.
Maybe the issue is that we overload the concept of a function with an entirely different thing, a Future / Promise. Maybe if the syntax would have been entirely different too, it would have been easier to understand. We tend to have different syntax for different things.
I’m hesitant towards not distinguishing different things anymore and let the underlying system “figure it out”. I’m sure this could work as long as you’re on the happy path, but that’s not the only path there is.
I think with stackful coroutines you lose low-overhead interoperability with C. Also, it possible to use stackless coroutines without introducing async/await 'colors'.
Concurrency's correct primitive is Hoare's Communicating Sequential Processes mapped onto green threads. Some languages that have it right are Java (since JDK17 - Java Virtual Threads), Go, Kotlin.