3 ms·
I'd like to hear about the distinction between promises/futures and async/await. The former is absolutely a useful abstraction, requires no special features to
by couchand 7y ago
I'd like to hear about the distinction between promises/futures and async/await. The former is absolutely a useful abstraction, requires no special features to implement, and thus is already widespread. The latter adds syntax support for one particular monad, and in the process adds magic that obscures the fundamental operation.
My experience with JS async/await code in the wild is that developers promptly forget (or never learn) that it desugars to a promise, forget all the tools that can be used for managing complex asynchronous code with explicit promises, and then fail to properly account for the asynchronicity of their code.
I would hope that Rust, with a type system and a community focused on engineering skills, would be able to avoid some of these pitfalls, but I'm not so sure about that.
- steveklabnik 7y agodo notation in a language without a GC is an open research question. Some believe it’s impossible. However, we have people using Rust today that need help. We could ignore these production users and say “sorry, it’s gonna be who knows when”, or we can ship something that solves their problems. Even shipping async/await itself took four years, even though it’s a technique that’s well known!
- couchand 7y agoMy working theory is that async/await syntax causes more problems than it solves. I've read through the RFC and many of the comments, but I don't see anywhere a cogent argument why the syntax is needed -- that seems to be taken on faith. I've written and reviewed boatloads of promise-using JS code, a fair amount of promise-using C++ and task-using C#, and some future-using Rust. I've never seen a case where the value added by async/await exceeds the potential for issues. The goal seems to be make asynchronous code look like synchronous code. In my experience, developers do in fact act like it's synchronous, forgetting the critical fact that it's not. The supposed feature is a massive bug in everyday usage. EDIT: reading through the RFC and comments, I now realize that async fn is NOT actually being implemented as sugar on top of future-returning functions, but in fact is more like a Python or ES6 generator. There seems to be a disagreement as to whether an async fn is really just a future-returning function: I see the claim made both ways, and haven't found an authoritative source yet. Confusing! EDIT2: > do notation in a language without a GC is an open research question. Some believe it’s impossible. I don't understand how GC comes into play. Isn't the do notation simply syntactic sugar for bind sequencing?
- steveklabnik 7y ago> I've never seen a case where the value added by async/await exceeds the potential for issues. There are many cases in which you can write safe code with async/await that you cannot write safely with raw futures. See here: http://aturon.github.io/tech/2018/04/24/async-borrowing/ http://aturon.github.io/tech/2018/04/24/async-borrowing/ > I now realize that async fn is NOT actually being implemented as sugar on top of future-returning functions, but in fact is more like a Python or ES6 generator. There seems to be a disagreement as to whether an async fn is really just a future-returning function: I see the claim made both ways, and haven't found an authoritative source yet. Confusing! The return type of an `async fn` is an `impl Future`. Additionally, you can `.await` on any `Future` inside of an `async fn`. That means it feels like sugar for Futures, which is why people say that. It's a good mental model. However, the current implementation is a generator internally. > I don't understand how GC comes into play. Isn't the do notation simply syntactic sugar for bind sequencing? It is in a language with garbage collection. Things get much more dicey when you don't have those things. For example, Rust has three kinds of closures, not one. You have to deal with references and lifetimes. Rust is generally imperative, and so you have to figure out how break, continue, etc, all work. Due to all of these things and others, the various monadic types Rust have don't actually have the same signatures as each other. I believe there are some more issues, but that's what I've got off the top of my head. That doesn't mean that this is actually impossible; it means that it requires more research to even figure out if it is possible. Rust doesn't have to solve every problem perfectly. There will be other languages in the future. Maybe Rust++ will have this kind of tech.
- couchand 7y agoI had not seen that particular example, thanks for providing the citation. Perhaps I'm just being stubborn here, but allow me a moment. Isn't the core issue there that the executor requires the future to have a lifetime of 'static? Aaron explains it: > ...because they are tossed onto executors like thread pools and hence not tied to any particular stack frame. It's surely a huge undertaking, but wouldn't a more nuanced lifetime analysis solve the underlying problem here? I don't think the issue is that any other way to write this example is unsafe, it's that it's running into a limitation of the borrow checker. What's required of an asynchronous borrow? The same thing required of any borrow: exclusivity. Sure, the async/await lets you write code that "looks synchronous", and gives the lexical cues the compiler needs to enforce the borrow. But does that mean it's the only way? The counterexample in Aaron's article even relies on async/await! That makes me highly suspicious that the need for it was taken as a given and never sufficiently challenged. The core issue in that example is that the borrow needs to be held until some point in the future that can't truly be determined lexically. That says to me that it's not a borrow in the traditional Rust model. It is absolutely a useful borrow to check for, but why go about it backwards by only allowing such chronological borrows in such narrow circumstances? Moreover, the example isn't realistic. The model that should be used for reading from a socket is a stream. > The return type of an `async fn` is an `impl Future`. Well, it is and it isn't. For instance, it appears that one can't await on a plain function that returns an `impl Future`...