
Rust async-await status report 2 - fanf2
http://smallcultfollowing.com/babysteps/blog/2019/07/08/async-await-status-report-2/
======
spinningslate
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?

~~~
steveklabnik
_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.

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

~~~
steveklabnik
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=...](https://www.techempower.com/benchmarks/#section=data-r18&hw=ph&test=plaintext)

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.

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

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

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

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

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

~~~
empath75
Nothing is trivial with rust.

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

~~~
hu3
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?

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

~~~
hu3
Thanks for explaining steve. I guess these things become second nature with
practice.

~~~
steveklabnik
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 :)

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

~~~
steveklabnik
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!

~~~
Dowwie
oh in that case, it might!

------
randyrand
This has been asked already, but what are the benefits to declaring async in
the function definition and not at the call site?

~~~
steveklabnik
I'm not sure why you would do it that way. How would you know which functions
can be async and which ones cannot?

------
krircc
[https://news.ycombinator.com/item?id=20402082](https://news.ycombinator.com/item?id=20402082)

------
samirm
Thanks for the update, but why do you have to repeat yourself so much in the
post? :/

