Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I can confirm there are at least 2 of us. I suspect a few more given previous discussions on the topic, e.g. [0].

Steve Klabnick provided some useful context on that thread for the Rust decision specifically. More generally though, you've nailed my primary misgiving:

> Synchronous or asynchronous is something up to the caller to the decide – not to the function itself.

Erlang/Elixir and Go place the decision with the caller, not the callee. That just seems much more sensible.

An alternative is perhaps to see async/await as dataflow going through the awkward teenager stage. Maybe one day we'll wake up and languages in general will have fully embraced dataflow variables as first order constructs.

[0] https://news.ycombinator.com/item?id=20400991



In go (and anything else with green threads), you pay the price for not putting await in front of stuff by introducing a whole new semantic concept of different threads which must now communicate with each other with other specialized data structures etc. Multithreading is useful when you will actually be using the CPU’s full capacity, but is an enormous extra abstraction if not. With async/await you just have to introduce a little extra syntactical clutter.


You need synchronisation primitives with async/await too. If you make every function async then you basically have the same as 1:N green threads. If you have pre-emptive threads or true multithreading with a N:M model then you do need to be more careful with synchronisation, but in principle this is orthogonal to async/await vs green threads.


Can't a compiler easily spot most cases and convert code from multithreaded to async style under the hood, at least in idiomatic Go?


Count me as third.

I dislike how this stuff "infect" all your code calls:

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

And the problem is rust is that we already have other stuff that infest everything: Borrows, mutability, ownership...


> And the problem is rust is that we already have other stuff that infest everything: Borrows, mutability, ownership...

I don't think anyone disagrees that the annotation of these concepts play a very heavy role in the type system and cognitive burden of writing code. But there are really three alternatives you can do here:

1. Do nothing, and rely on programmers to manually remember how to do all the necessary bookkeeping themselves. This is better known as "C", and the sheer amount of CVEs and other problems in code written in C is ample evidence that this approach just doesn't work.

2. Make the runtime do the automatic programming. JS is the exemplar of the pattern here. But this approach requires the VM to do garbage collection itself (which adds uncertain hidden overhead), generate code to handle arbitrary shapes (which can result in hidden performance cliffs). And heavily dynamic languages also turn compiler errors (such as fat-fingering a variable name) into runtime errors that can be accidentally ignored.

3. Shift the annotation into a mandatory part of the language, as Rust does. It makes the cognitive burden of writing the code higher, it requires more coding, but the resulting code tends to have the highest performance and lowest bugs of any of these approaches.

The tradeoff you have to make is between "no safety", "compile-time safety", and "runtime safety"; the costs you have to weigh are the costs of testing to uncover bugs, the costs of programmer cognitive and annotation burden, and the (often hidden!) costs of the runtime to dynamically enforce the conditions.

Rust has chosen to live in the camp of low implicit runtime cost and high compile-time safety, and it is really to pay the price for that with extra annotation burden. You may disagree with that choice, but it is a conscious choice that has been made.


> 3. Shift the annotation into a mandatory part of the language, as Rust does.

This is what I want. I just not like the specific implementation of awaits. I'm not much against it: i could live with it (I have used C#/F#/JS that is similar).

The thing is that still split the world even if you don't want it.

I think CSP/Actors have lesser cognitive load but of course I don't know how make them work without a runtime..


> I dislike how this stuff "infect" all your code calls

Unlike in JS, it really doesn't. In Rust, at any point you have the option to run a Future returned from an async function to completion in a sync function (which blocks the thread) and stop the spread of async.


I don't like the "what color is your function" post, because it presents Go's M:N solution as something new, when in fact it's just a user-mode implementation of threads just like NGPT was back in the early aughts. In fact, we have M:N in Rust too. It's not sufficient for people's performance needs, which is why async I/O exists. Either 1:1 threads or mioco might be sufficient for your performance needs; if so, you're free to use them and you won't have to worry about red or blue functions!


I re-read and don't see where Go is claimed to be "something new".


Infests sounds like it's a negative, there's always go if you'd prefer to have a GC and no generics. Those have other well known costs as time goes on


All of those languages have a runtime that enables this sort of thing. That's not the case in Rust. It's a bit of an unfair thing to knock it for, given the restrictions.


Particularly with Erlang with beam VM which can be heavy duty for smaller stuff. Which is totally fine for the use-cases and comes with tons of benefits. Ie. everything can be a async and decoupled with actor-style processes since it's such a low investment both CPU/memory wise and cognitively for the developer... with plenty of abstractions and well-documented best-practices to help you manage them (documentation and top-level support from the language creators itself is always valuable).

While also making your software more resilient to failure because error handling is easier as you can contain the code into small processes, communicate through messaging, let processes fail, and handle/restart them from the supervisor. Using the OTP supervisor model encourages you to think through those things, like error handling and the lifecycle of your code, so it doesn't impact other parts of your running app. A lot of exception handling in other languages is often bolted-on after-the-fact and isn't part of the typical design, at least for more inexperienced developers.

It's best to commit fully to either a full-featured VM or Rust/c low level OS style IMO.

The latter group can still support a more complex actor model with libraries and frameworks (ie, https://actix.rs/) which is more flexible but nothing beats having it be the 'standard' philosophy of the language and fully baked into the runtime.


Kotlin has an interesting midpoint between async/await and built-in lightweight threads. With async/await calls are async by default and if you await them they become synchronous. In Kotlin and with built-in lightweight threads this is reversed, but in Kotlin the callee still has a different function signature depending on whether it uses async internally or not.


I believe that this is how Scala does it, too. The caller provides an execution context that defines how the task will be run.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: