92 comments

[ 2.7 ms ] story [ 159 ms ] thread
(comment deleted)
I really appreciate the progress here!

But I'm wondering if the return type of the function in the code example is not introducing too much complexity for a Rust novice trying to develop a simple highly concurrent HTTP service:

    async fn process(data: TcpStream) -> Result<(), Box<dyn Error>>
Update: I read that one can simplify this using for example the Hyper library:

    async fn serve_req(_req: Request<Body>) -> Result<Response<Body>, hyper::Error>
Would a novice to the language implement a highly concurrent HTTP service as their first ever task? I don't think so, most people start with "Hello world" programs.

To understand the entirety of that first signature you need to grasp is:

1. The Result type, which returns the result of a successful computation or an error.

2. The Box container, which is basically just a safe pointer to some memory

3. Rust's way of doing polymorphism.

These are indeed some complex topics but I doubt most people start at that level. Sure, an absolute beginner would be dumbfounded by this, but working through even the first few chapters of the Rust book gives you enough knowledge of Result and Box to let you figure out what is going on.

> Would a novice to the language implement a highly concurrent HTTP service as their first ever task?

Why not?

With Node or Go, this is trivial.

Nothing is trivial with rust.
Many things aren't trivial, but some things are! For instance, the following is a program that uses a well-defined, user-friendly api for regex pattern matching.

   extern crate regex; // 1.1.8

   fn main() {
        let pattern = regex::Regex::new("(?i)resource not found").unwrap();
        let msg = "resource not found".to_string();   // didn't have to convert to String as &str suffices
    
        if pattern.is_match(&msg) {
            println!("found pattern in msg");
        } else {
            println!("did not find pattern in msg");
   }
}

Clearly, this is an easy example and when used in the wild, you'll have to account for error handling. However, whatever isn't trivial exists for a good reason. Trivial comes at a cost. One thing I like about Rust is that it lets a programmer decide what costs to assume.

Perhaps we have different definitions of trivial but I don't find it trivial to cast a string to a string in variable declarations:

    let msg = "resource not found".to_string(); 
Is it necessary?
You're not casting a string to a string, you're casting a &str to a String.

> Is it necessary?

In this code, it's not actually necessary, your parent converts it back to a &str later, when passing it to is_match.

In other cases, it's very necessary; they're two different types, with different properties.

Thanks for explaining steve. I guess these things become second nature with practice.
Yep!

Rust has a few "rules of three" that appear again and again. This is an instance of there being three kinds of types: T, &T, and &mut T. That is, owned, borrowed, and mutably borrowed. In most examples of this, "T" is the same thing in all places, that is, i32, &i32, &mut i32, but in this case, "str" is built into the language. So you have String, &str, and &mut str. The naming is slightly off, because you're coming into the conflict of String being a standard library type, and str being a language type. We almost unified them before 1.0, but there wasn't a ton of benefit.

The point is, yes, it is a bit confusing at first, you're totally right. I actually re-did the entire table of contents of The Rust Programming Language to re-focus them on strings due to this. But you're also right that after some practice, it's a non-issue that you never really spend mental effort on.

The trick is, how many people will be willing to spend that time? Can we reduce that time? Time will tell :)

Actix-web actually makes things a little bit easier. Here is their "Hello World" example https://github.com/actix/examples/blob/master/hello-world/sr...

The HTTP Handler (the index function) returns some plaintext and prints to the server's console. Removing the print makes the function simpler than the one in the main article.

Actix Web was designed from the get-go to be a highly concurrent HTTP framework, and I think it is the most mature Rust web framework.

Of course, the boilerplate main still needs to be written, and the way it does things is a bit more profound. But at a high level, I believe it is quite clear that it is handling logging, URL routing and connecting to a port.

I don't see why not, making a simple HTTP server is fun and interactive, doesn't take much time and you can learn a lot if you don't know much about the language yet.

Seeing that function signature, I can imagine why a lot of people avoid Rust just based on first impressions.

Not to mention, you wouldn't even have to see a signature like that in the first place. Many of Rust's frameworks can accept a variety of signatures for handlers, so for example, with actix-web:

    use actix_web::{web, App, HttpRequest, HttpServer};

    async fn index(req: HttpRequest) -> &'static str {
        println!("REQ: {:?}", req);

        "Hello world!"
    }

    fn main() -> std::io::Result<()> {
        HttpServer::new(|| {
            App::new()
                .service(web::resource("/").to_async(index))
        })
        .bind("127.0.0.1:7879")?
        .run()
    }
Five small notes:

Actix runs on stable Rust, and so does not support async/await directly yet; this is what it will look like when it does. You need to write a bit more code to map between the 0.1 futures and stdlib futures to use it today. This will go away once async/await is stable.

The async/await is not needed for something that simply returns a string, of course, but to keep it as close to the original example as possible...

The `'static` is a bit unfortunate, but if you just write &str, the compiler will tell you to insert it. A small speedbump but shouldn't stop a newbie in their tracks entirely.

This is an infallible handler, since we're just returning a string. You can use Results and such as well, when your handlers need it, but it doesn't have to come into play for the simplest of things, which reduces the overall burden. A lot of web frameworks in Rust let you return non-Strings too, which is actually a hard requirement or else you can't serve things like images. It's just that "hello world" is a string...

It didn't make it on HN, but the latest Techempower benchmarks show Actix starting to run away with the competition: https://news.ycombinator.com/item?id=20399773

Thank you for your reply, that was exactly what I was trying to show when I linked the Actix Web example in one of the child comments below!
> To understand the entirety of that first signature you need to grasp is: ...

...4. The unit type (), which is basically a 0-tuple, i.e. a type that carries no information, and generally replaces the use of `void` in C-like languages. (Rust also has a proper void type, i.e. a type for something that cannot occur, but that's denoted by ! and is quite different from the unit type.) It's not a hard concept, but it may be a bit unfamiliar if you havent worked with functionalish languages before.

I'd argue that hiding what's going on for the sake of "simplicity" ends up confusing a novice more when it means the function ends up behaving inconsistently. E.g. in many languages a novice might see a function that returns just "Response", but when they call it it actually "throws" an "exception", which is a whole different concept that you have to understand and that completely destroys the simple "function evaluates to its return value" model.

Result has the advantage of being a plain old datatype that's just written in normal code - it follows all the normal rules of the language, and the user can probably click through and read its implementation for themselves.

I think the Result type is quite easy to understand, and it's central to Rust error handling strategy.

The main difficulty is we also have to understand the purpose of the dyn keyword and why the Error must be boxed.

Anyone writing serious Rust will have to learn this sooner or later, but maybe this is a lot to learn "upfront" :-)

> The main difficulty is we also have to understand the purpose of the dyn keyword and why the Error must be boxed.

Box<dyn Trait> is just an owning opaque pointer to something that implements the Trait interface. This kind of stuff is done all the time in C/C++ because it's just so darn useful, even though it requires a bunch of boilerplate code to make it work properly. (It's also arguably implied in the semantics of other languages, even though it's not directly marked in the code.) In Rust, it's just a line of code and the compiler provides checks that ensure you're writing something reasonable. (And of course you can dispense with the whole boxing mechanism when it's not needed, same as in C.)

It's a great milestone, and hopefully async methods not far behind in the pipeline. I might be able to port my work to async-await syntax with the first release to stable in ~1.38 but I may wait longer until async methods are addressed. I want to encapsulate state with async behavior.

Updated (as per Steve): inherent methods will be avail and may address my reqs

Note that async inherent methods will be part of the MVP; it's trait methods that are much harder and won't. Not sure if that fits your requirements or not!
oh in that case, it might!
Genuine question: can anyone comment on which alternatives to async/await were considered? And if so why they were rejected?

It’s proliferation puzzles me: c#, python, node, rust and no doubt others. Yet it seems like a poor abstraction.

1. The cognitive overhead. Whilst the example might be intended to show off power, that function signature is not at all easy to parse mentally. Even allowing for generics it’s still a lot more difficult to think of a function that returns a future that will return the result at some point (as opposed to a function that returns a result synchronously).

2. It’s declared at the call site - not by the caller. As the writer of a function, how am I supposed to know whether the majority of callers will get sufficient benefit to offset the increased cognitive overhead? It can also have viral tendencies: a function that calls an async function is async itself, and so on. (Not sure if that’s mandatory in all language implementations). I’ve seen more than one code base where async was like a virulent rash.

Personally I find Actor model concurrency so much easier to deal with (e.g. Erlang). The rules seem significantly more straightforward and easier to deal with mentally.

Data flow concurrency (e.g. FRP) also seems much easier to reason about.

Async/await by comparison seems like a poor substitute. So I’m puzzled but intrigued by its growing popularity.

I can imagine it’s easier to implement in the language (compiler/runtime), which might be key for rust. I can also see that it’s an incremental step for procedural languages. But then surely it’s a wise investment to build good abstractions in the language - even if it’s (a lot) harder - rather than burden the programmer with more complexity?

What am I missing?

Everything was considered. This has been the most discussed Rust feature to date, but a serious margin.

I'll try to give you some answers from my perspective; I am not on the language team, nor have I done any of this work myself, but this is what I remember, and see from the outside.

The short answer is: it's the only way to accomplish the task that fits into Rust's requirements. The top relevant ones are speed, lack of required runtime for non-async code, and zero-cost C interoperability. It sounds to me like you're coming at this from a "what is the simplest thing for a programmer to use" angle. That's a totally valid angle, but Rust is focused on ease of use only within the context of other requirements. Notably, Erlang's model, while it has excellent properties, falls down on all three of these other requirements. That's totally okay in the context of Erlang, but is not acceptable in the context of Rust.

Thanks. I suspected run-time constraints might be a reason so that makes a lot of sense.

> Notably, Erlang's model, while it has excellent properties, falls down on all three of these other requirements.

Being nitpicky, but I'd argue Erlang only falls down on speed where it's computationally bound. Where it's IO bound (e.g. a prototypical chat server) Erlang has excellent speed characteristics - where speed is measured in concurrency and latency.

But I absolutely get that's not the interpretation of "speed" you meant.

Appreciate the explanation.

While it is true that I'm mostly talking about computation speed, that's not the only thing. It is true that Erlang is good at this, but I'd like to see some real numbers here. In benchmarks, this is almost never the case. For example, if you take a look at techempower's plaintext benchmark, which should purely be about IO: https://www.techempower.com/benchmarks/#section=data-r18&hw=...

Phoenix, which is in Elixir, does 2.7% of the requests of the Rust frameworks. Chicago Boss, in Erlang, does 0.2%. Is this a realistic benchmark? I don't know; I can't say if the code for either of these implementation is good or not. But if it's a silly error, I think it would be good for Elixir/Erlang advocates to fix this. Early Rust implementations had extremely poor showings too.

I tend to think of Erlang as being extremely reliable, with acceptable performance, rather than being the fastest IO language in the West.

As an admin of the elixir slack, i have to handle that one a lot.

It is not really realistic at all no. But mostly because the benchmark is not realistic of a production load. That said we should do better.

The main reason it is not fixed is that the few members of the community that tried to fix it had a really bad experience interacting with the repo and getting feedback of impact and simply lost interest. And we have not have people motivated enough so far because they do not really care about the benchmark.

Ofc it matters to people that are not in the community but well...

That said, yes we would never be close to Rust anyway (probably) but we could probably have a far better standard deviation than Rust. Something that tend to matter in production a lot but people forget reading benchmark is the spread of the response time vs the average one.

We should also perform better in a noisy environment like the "cloud" one.

There are hopes that the recent EEF marketing group could help find volunteers or finance someone to fix it.

Yep, that makes perfect sense. I've had good experiences contributing, but it can be tough...

It's also 100% true that benchmarks are not representative of production loads. Sadly, it's also 100% true that programmers make decisions based on benchmarks that aren't representative of production loads. It's a real catch-22 situation.

> Being nitpicky, but I'd argue Erlang only falls down on speed where it's computationally bound. Where it's IO bound (e.g. a prototypical chat server) Erlang has excellent speed characteristics - where speed is measured in concurrency and latency.

At the scale where asynchronous IO becomes interesting, speed is computationally bound. Think maintaining tens of thousands of TCP connections. You can either spawn a single OS thread for each connection, which gets prohibitively expensive with regards to memory consumption, or you can use asynchronous IO and have a couple of native worker threads, approx. 1 per CPU core.

If Erlang can handle, say, tens of thousands of TCP connections with one Erlang thread/actor per connection, then only because it is using an implementation of asynchronous IO in the background.

> If Erlang can handle, say, tens of thousands of TCP connections with one Erlang thread/actor per connection, then only because it is using an implementation of asynchronous IO in the background.

Yes, it can. That's one of the BEAM's key strengths (the Erlang VM). It presents a simple, uniform concurrency abstraction - the process - to developers. Note these are application-level processes, not OS level. They're very lightweight and it's possible to spin up tens of thousands on a single machine. The VM multiplexes user processes over the underlying hardware (threads/cores). More here [0].

[0] https://stackoverflow.com/questions/2708033/technically-why-...

We used to have M:N threading in Rust's runtime -- this is one other way to achieve what we wanted. It imposed a cost on all programs, including those which weren't using it, and iirc it didn't fit well and wasn't flexible enough (I'm not sure of this) and it got removed.

Async/await/futures with library level scheduling is basically the only way to get the benefits of application threads in rust without such downsides.

But with other downsides such as making application code less easy to debug and maintain.
Certainly, but if you're willing to commit to one runtime for your app, there are plenty of other choices: Go, Erlang, Kotlin, Swift, Typescript or Dart might be better bets. Web browsers and mobile phone apps do well with this approach.

Rust (along with C or Zig) has an interesting use case for building large libraries that can be reused across different runtimes via foreign function calls. It's paying some usability for the library maintainer for being able to work in more environments.

To speculate a bit, I expect we will eventually see some interesting new runtime environments built out of the various libraries in the Rust ecosystem. (Along with C of course, which is how runtimes are built now.)

Maybe we will see a trend away from reimplementing all the libraries in whatever the hot new language is, in favor of building on a more persistent set of stable libraries? (Already true today with C, but the Rust ecosystem could evolve into something more cohesive.)

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.

do 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!

My 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?

> 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/

> 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.

I 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`...

> Isn't the core issue there that the executor requires the future to have a lifetime of 'static?

Yes, and no. That is one of the factors. Another, and IMHO, bigger factor is that the lifetime needs to be 'static only as the task starts to execute. This is why Pin, etc, came into existence. This is due to self-referentiality.

> It's surely a huge undertaking, but wouldn't a more nuanced lifetime analysis solve the underlying problem here?

Yes, we're back in the realm of "open research question."

> What's required of an asynchronous borrow? The same thing required of any borrow: exclusivity.

Most borrows are not exclusive.

> That makes me highly suspicious that the need for it was taken as a given and never sufficiently challenged.

With respect, it is just as easy for me to say "This makes me highly suspicious that you have not investigated how all of this works and that's why you think it's easy."

> 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.

Rust's borrows have not been lexical since last year.

That's fine, of course! It's not like getting into this stuff is easy. But the team working on this has a ton of experience; multiple PhDs, etc, etc. This is the most investigated feature of Rust ever. Again, yes, maybe it is possible, but it's not within the realm of a "just".

Additionally, none of any of the stuff in this comment this solves the original issues with do notation.

> it appears that one can't await on a plain function that returns an `impl Future`...

Sure you can: https://play.rust-lang.org/?version=nightly&mode=debug&editi...

> the lifetime needs to be 'static only as the task starts to execute.

It would seem the lifetime needs to be coerceable to the lifetime of the executor, but as far as I can tell never really should need to be 'static.

> Most borrows are not exclusive.

Sorry, I meant, exclusive mutability.

> Rust's borrows have not been lexical since last year.

As I understand it, the borrows are not lexical in the classic sense of lasting until the end of the function, but they are still determined completely lexically, correct? Here the async/await syntax is provide lexical cues regarding the sequencing of asynchronous code, which is how it helps the borrow checker, right?

> But the team working on this has a ton of experience; multiple PhDs, etc, etc.

Of course. You've all done a great job producing a solid language! I'm not questioning the technical chops. And I don't think I said "just", I appreciate the scale of the problem. I'm just arguing about a tradeoff that I have not been able to find much discussion of.

It's great to improve the ergonomics of the language. We need more people to want to use it! Is it possible that this is going too far? It sounds like it introduces an awful lot of magic.

> I'm not questioning the technical chops.

No worries then; a lot of people have made very similar statements in the past, and that's the place they're coming from.

> I'm just arguing about a tradeoff that I have not been able to find much discussion of.

I am still not 100% sure what exactly it is you're talking about, at this point, to be honest. I think it's "why do we need async/await and not just write manual futures". I've pointed out several things. Is there something else?

> It would seem the lifetime needs to be coerceable to the lifetime of the executor, but as far as I can tell never really should need to be 'static.

Some executors, like Tokio, are mult-threaded with work-stealing. A task (chain of futures) may start executing on one thread, be waiting, and then get moved to another thread before continuing execution.

> the borrows are not lexical in the classic sense of lasting until the end of the function, but they are still determined completely lexically, correct?

I am not totally sure what you mean by "completely lexically" here. It is true that NLL means that borrows can exist for only part of a lexical scope, but not more than a lexical scope. Or rather, the "more than" part is already taken care of by lifetimes anyway.

> Is it possible that this is going too far? It sounds like it introduces an awful lot of magic.

Most programmers don't really mind magic as much as you'd think. I mean, sure, in some sense, it's a lot of magic, but the result is that instead of writing

    use std::future::Future;
    use std::task::{Context, Poll};
    use std::pin::Pin;
    
    struct Five;
    
    impl Future for Five {
        type Output = i32;
        
        fn poll(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Self::Output> {
            Poll::Ready(5)        
        }
    }
    
    fn returns_five() -> impl Future<Output=i32> {
        Five 
    }
I write

    async fn returns_five() -> i32 {
        5
    }
And, the intuition that "async fn returns an impl Future that I don't have to implement" is simple enough and accurate enough that it feels like even less magic.

In practice, the people who have been writing tons of networking code don't see it as too much magic; they see it as a significantly nicer way to write the code they already have to write. The ergonomic difference is so large, that the delays in shipping have caused fairly significant community strife and pressure. It is possibly too hyped. Writing manual futures is so, so, so painful.

I'm curious if the team ever considered an inverted approach. I've read through many of the discussions, and don't think I ever saw this suggested. I'm sure there must be a reason not to do it, but my Rust isn't advanced enough to tell.

It seems to me that you'll generally await futures in high-level code. Keeping values around as futures is the minority case. So instead of asking to await one could automatically insert awaiting code whenever a future needs to be converted to a real value. Instead of this:

  async fn process() -> Image {
    await process_image(await fetch("http://example.com/foo.jpg"))
  }
...you could simply have this:

  async fn process() -> Image {
    process_image(fetch("http://example.com/foo.jpg"))
  }
...which would await both the inner and outer futures: process_image() expects an Image, and fetch() returns a Future<Image>, so it can be automatically awaited and coerced.

Sometimes you do want a future when you don't want to wait right away, e.g. when storing a bunch of pending operations or something. For that, one could tell Rust not to automatically coerce futures:

  let pending: HashMap<string, Future<Image>>
  // ...
  async fn process() {
    pending[key] = defer process_image(fetch("http://example.com/foo.jpg"))
  }
...although this, too, could presumably be done transparently for you, since Rust knows you're assigning to a future.
Those hardly look different. From the context of the parent comments I thought you were going to suggest something completely different, but it looks like it's just a small syntax change.
The semantics are clear and have already stabilized. The discussions the last year have almost entirely been about syntax.
Yes, this was suggested. I don’t remember the exact reason it was rejected, other than maybe being a more unusual choice with no significantly clear benefit.

I think part of it too was that this works better if your futures were to execute immediately like promises; this would make a lot of code serial by default.

I think the primary consideration is interoperability with C. Being able to call into C from Rust and into Rust from C is an important feature. Systems with a heavy runtime and special stack layout like Erlang make this harder.

Rust originally had a heavier runtime with a green thread implementation, and it was removed. I believe one of the major considerations at the time was bare metal programming on microcontrollers.

Async seems virulent.

But a few days ago I was writing a small TCP app, and I could not find the timeout argument to Python’s asyncio open_connection.

Then I realised I didn’t need it: I could simply create more complex behaviour by combining async primitives. I don’t even want to imagine how much code I’d need to write in order to implement something like that thread-wise (given that a library simply forgot to add it).

A couple of random thoughts:

1. If it's like Javascript, async and await basically boil down to converting to and from Promises. There's no reason to have to transitively perpetuate `async` annotations: you can just deal directly with a completely normal Promise object instead. (But you soon find you like `async` better :-))

2. The “caller decides” model is actually considered good practice in Go: expose synchronous primitives, and let callers compose them with Goroutines if they want to.

> If it's like Javascript

It is and it isn't. That is,

> async and await basically boil down to converting to and from Promises

In Rust, they basically boil down to converting to and from Futures. However, the major difference is that while in JavaScript, promises start running upon creation, in Rust, a Future doesn't start executing until you submit it to an executor. This has many implications...

What’s the difference? Futures and promises come in pairs, right?
I'm not 100% sure what "come in pairs" means, exactly. Here's a sort of quick rundown.

A Promise is in one of these states: pending, fulfilled, or rejected. When you create a promise, you set a callback function that is the "body" of the promise, and can transition it into the fulfilled or rejected state by calling either of two callbacks passed into it. When you create a Promise, it's in the pending state, but the runtime sets it up to be executed, and when a tick occurs, it will start executing automatically.

In Rust, a Future is something with a poll method that returns either Pending or the final value. If you want JavaScript's behavior around failure, you make the final value a Result, it's not built-in. In Rust, unlike in JavaScript, a Future is inherently inert. Nothing happens until you actively call the poll method of said future. This means that, unlike in JavaScript, you have to either call poll yourself (not recommended) or pass it to something that will. We colloquially call those things "executors." This can lead to surprising behavior if you're used to the JavaScript model; many JS programmers coming to Rust will create a future, and then be surprised when their code compiles but doesn't work. It's usually that they forgot to submit things to an executor.

In both cases, you can chain these things together, which is how they're usually used to produce some bigger computation.

Okay, so from an interface perspective, that's the difference. But the difference goes a bit deeper than that. For example, since Rust futures are nested when you chain them, and because the language is always AOT compiled, a chain of Futures compiles down to a state machine with a perfectly sized stack. Whereas a Promise has a separate object for each step in the chain, with all of the excess allocations and such that implies. I am not 100% sure what the state of the art is in optimizing promises, but in Rust, you always get the most optimized thing, since it builds off of everything else in the language.

Okay that's all I've got for now, this is already too long. This is a complex topic...

> If you want JavaScript's behavior around failure, you make the final value a Result, it's not built-in.

Fun bit history: the first pass at Futures did return an error type explicitly: https://docs.rs/futures/0.1.28/futures/future/trait.Future.h.... IIUC the idea was that Futures were for performing I/O which is effectively always fallible. I assume that it's an easy design decision to make looking at all of the other languages with similar designs. I'm very glad Rust has nicely orthogonal "deferred"/"fallible" types now.

> promises start running upon creation...

> ...until you submit it to an executor

That's not much different. Don't call the function then which returns that promise :)

Or you can write:

    sendRequest(thingThatReturnsPromise);
Sure, it’s easy to remember once you know it. It’s a very common foot gun for the JS folks coming over, and can also affect how you structure your code because of it. Code that runs in parallel in JS will be sequential when naively translated.

I’m in the process of writing a blog post about this...

Thanks btw for the warning, I'm learning Rust slowly by going through the awesome Rust book.
That makes sense. I'm glad you're standardizing all this early, unlike Python which created an entire Twisted parallel universe for everything and only standardized later.
Yeah, we were keenly aware of these splits happening and so worked really early to figure out how to not split the ecosystem. It’s been a long hard road...
Any chance you've written about specifically trying to foresee and prevent a split (Rusty? Rusted?) ecosystem? As an armchair PL, amateur SIGPLAN kind of person, I would be very interested to hear about your experiences. I couldn't find anything after a cursory search, though I did see a couple of posts where you briefly but explicitly mention your choices.
It’s mostly “traits are how interop is done, if there’s an ecosystem of multiple competing things, try to pull out common traits for folks.”
(comment deleted)
In Javascript you can make anything into a lazily evaluated expression by wrapping it into a function (sometimes called a "thunk").

// eager

new Promise(resolve => resolve(foo))

// lazy

const bar = () => new Promise(resolve => resolve(foo))

An executor would be something that takes n functions that return Promises and calls them.

Sure, we do this in Rust too.

I’m not sure what benefit this would bring you in JS.

It's most useful with anything sending multiple requests. Most often you have all the data to perform all requests beforehand, but you can't just fetch (or whatever xmlHttpRequest abstraction you like) to send all requests at once eagerly because of: performance in the browser (running something like 10 simultaneous ajax requests in a single tab often can really lag it) and rate limiting in the back end. So it's better to prepare all requests wrapped in a thunk and then you can execute them at will. You can really optimize as you like, like running some in parallel, secuentially or in batches. Of course this is for really custom cases, more often than not you are best served by something like Promise.map in bluebird which gives you a concurrency option. (http://bluebirdjs.com/docs/api/promise.map.html).
We already have the actor model in Rust, via threads. Some people want better performance than what threads can provide. M:N was tried in Rust and the performance was found to be insufficient (and there were other downsides too). So async/await was the next best option.

If you want the actor model, you can use it! There is nothing stopping you from going thread-per-connection in Rust. It's quite fast on Linux for all but the most demanding workloads. If I were writing an ordinary network service expecting moderate traffic, I'd probably go thread-per-connection.

I'm actually asking this sincerely; hasn't this experiment already been run with Apache vs. Nginx, with Nginx coming out literally orders of magnitude faster by using epoll and using a limited number of "real" threads?
Maybe, but for most websites Apache was never the bottleneck, it was more than enough, which is I think the point GP is making.
> I'm actually asking this sincerely

Why do you have to say this?

Because on the internet it can be difficult to tell the difference between someone asking the question as some kind of "gotcha!" and between someone asking a legitimate question.

I tend to be a fairly sarcastic person so when I want to make sure it's unambiguous that I'm not, I prefix my questions.

The performance differences between Apache and nginx are attributable to more than that one design decision.

Besides, Apache is fast enough for most use cases.

Well, it depends what else the server is doing besides talking to the network. Low-overhead concurrency matters when you're basically just copying data to the network, like serving up static HTML, images, or video. Particularly when clients have slow network connections, a request can take a long time with nothing much happening on the server. If most requests are mostly idle, the server can handle many more concurrent requests with lightweight concurrency.

With heavyweight computations, CPU usage will hit 100% with a small number of concurrent requests, so a thread per request will work fine. Your server will get overloaded before you have too many threads and that load needs to be shed somehow, maybe by putting more servers online.

Even if you're using async-await, using threads for heavyweight requests is recommended anyway. An async function isn't supposed to hog the CPU; instead you outsource to a thread pool.

> Personally I find Actor model concurrency so much easier to deal with (e.g. Erlang). The rules seem significantly more straightforward and easier to deal with mentally.

I haven't used Erlang, but how does it deal with blocking calls? Say an actor decides to make a call to the DB. Will the runtime block the current thread the actor is running on?

The other alternative to explicit async/await, which permeates the entire call stack as you pointed out, is to have green threads, like Java's upcoming fibers (see project Loom). The runtime automatically swaps out the current fiber once it makes a blocking call and switches in another non-blocking fiber to continue making progress.

Erlang's VM runs a scheduler on top of a number of OS threads. If an actor blocks, that actor won't be scheduled to run until the blocking call is finished, but you won't consume a whole OS threads, and other actors will be able to run while waiting for completion.
> 1. The cognitive overhead. Whilst the example might be intended to show off power, that function signature is not at all easy to parse mentally. Even allowing for generics it’s still a lot more difficult to think of a function that returns a future that will return the result at some point (as opposed to a function that returns a result synchronously).

Disagree, because it's all compositional, which is the essence of taming complexity. You can understand what a Future is and then, separately, understand the thing the function is doing. It's similar to working with a container: a function that returns a list of results is more complex than a function that returns a single result, but it's tractable because you can understand the two aspects separately.

> As the writer of a function, how am I supposed to know whether the majority of callers will get sufficient benefit to offset the increased cognitive overhead?

If your function would need to wait, or calls an async function, it should be async; otherwise it should be sync. You need to define your threshold for "would need to wait" in your codebase - popular answers are any I/O, any network access, or any non-local network access - but you can answer that once and then the decision for any given function is pretty mechanical.

Of course ideally you'd be able to write general-purpose functions generically such that the caller could use them as either sync or async; unfortunately Rust lacks higher-kinded types.

> Personally I find Actor model concurrency so much easier to deal with (e.g. Erlang). The rules seem significantly more straightforward and easier to deal with mentally.

Actors are far too unconstrained to be able to reason about a codebase that uses them. You send a message to an actor, and what does it do? Could be anything. Is it equivalent to this other message? Maybe! You didn't get a reply, has something gone wrong? Maybe!

> Data flow concurrency (e.g. FRP) also seems much easier to reason about.

What distinction are you drawing here? FRP seems like something you'd build on top of Futures as far as I can see?

Java is attempting to avoid this complexity using Fibers like Go. It also makes most old school threaded code compatible without a bunch of work.

Rust was originally going to use fiber threading. The reasons they didn't are lost to me and probably everyone with the amount of bikeshedding async await has seen

> Rust was originally going to use fiber threading. The reasons they didn't are lost to me

Because it was found that you can just put this stuff in user-level libraries, it doesn't need to be part of the core language. This is what Tokio does, it provides a multi-threaded runtime that can be used together with Rust's async facilities. Romio is an experimental project which extends Tokio to work with the new async-await syntax and adds async-IO support, but this work will very likely be folded back into Tokio itself as soon as the underlying features reach stability.

Is there currently a reasonably mature Erlang-style threading library for Rust? Something that is to Rust what Akka is to Scala?
There are people playing with them, but I don’t know how mature they are. Actix is one example.
Would you be able to venture a guess why they are not more popular? I agree that basing Rust's concurrency on an Erlang-like model would not have been a good idea, for reasons mentioned in this thread. Nevertheless the sheer convenience of Erlang-style concurrency would nevertheless be useful in quite a few scenarios where Rust is used.
I'm asking because I'm considering developing an Erlang-like approach to message-passing concurrency for Rust.
I dunno, it just seems philosophically divergent. Rust isn't afraid of sharing, that's kinda the whole point.

actix was a big one, but actix-web ended up moving off of it, so I'm not sure what the state of it is.

Building it into the language has the advantage of enabling fiber code in all libraries. I think rust is making a huge mistake here.

The reason it's even possible to retrofit fibers into old Java code is because all the threading and async stuff is built into the language. Unless you're using native code enabling fibers will be as easy as switching out thread pools that your threads use.

The basic interfaces are built into the standard library, precisely in order to ensure compatibility across the Rust ecosystem. So if you're writing something that's compatible with the basic futures/async model, you'll be able to use any provided runtime with Rust. It may be possible to do even better in the future (use HKT to enable the exact same code to be compiled as either "sync" or "async"-based) but I'm not sure why you think that the previous model of making Rust into yet another heavy runtime-based language (with a handful of weird, tacked-on functional features that would not even help performance to any real extent given those prereqs, but still making the language even harder to grok than Haskell!) is preferable.
The challenge is going to be the same that C++ had until ISO C++ finally understood the way forward was to actually have them in the standard.

A bunch of third party libraries with different levels of quality not guaranteed to be available across all target platforms supported by the language.

There are two reasons: fibers with non-native stacks don't mix well with zero-cost FFI with C and other languages with native stacks. For example, Go suffers from this problem. The other problem is that N:M threads require a runtime and not having one is a hard requirement for Rust.
This is pure speculation on my part but... it seems to me that actors require a lot of immutable data sharing through message passing while Rust's whole raison d'etre is to safely share data access through intelligent pointer management and access rules.

Seems like forced message passing would be out of place.

The most efficient way of executing a non-I/O function is to just use the thread stack to store its data and optimize it for speed rather than for using little stack space.

However, for functions that can block indefinitely on I/O, then such an approach would mean keeping an inefficiently-large stack around indefinitely.

Hence, the better solution is to put the data on the heap, ideally in a manually engineered structure (i.e. "manual implementation of futures"), or for more ergonomics in one efficiently designed by the compiler (i.e. async/await).

Thanks for the update, but why do you have to repeat yourself so much in the post? :/
This has been asked already, but what are the benefits to declaring async in the function definition and not at the call site?
I'm not sure why you would do it that way. How would you know which functions can be async and which ones cannot?