Hacker News new | past | comments | ask | show | jobs | submit login
Understanding Rust futures by going way too deep (fasterthanli.me)
475 points by tempodox on July 26, 2021 | hide | past | favorite | 130 comments



Thank you once again for writing such an excellent article <3

In contrast to some other comments here, I love the narrative style. After all, these are half tutorials half entertainment - and the entertainment keeps it enticing. I would not have spent nearly two hours reading about the nitty-gritty of futures implementation details today if it weren't for the way you write about things.

To put it the other way - if I wanted to read a super condensed, concise reference of how these things work, I would, you know, read a reference. The narrative style of building things on your own, solving problems as you face them, picking tools and libraries on the way, feels relatable as the everyday lived experience of software development. It feels appropriate for the medium - it's a blog after all!

Besides, blogs are the perfect place to mention the tiny useful tools and libraries people might not have heard of. References and books might err on the side of being "neutral" and not making opinionated recommendations, but this can also lead to unknown unknowns. People don't know what they are missing out on! Luckily, blogs have more freedom here. Mentioning things like `cargo-edit`, `serde` and others can probably make some readers feel like "one of today's lucky ten thousand" - enjoying the newly-gained quality of life improvements.


Yeah! This article not just explains how async works, the author show research methods to learn how async works. How something works is transient knowledge, how to learn how something works is timeless priceless knowledge.

The methods include strategically inserted panic!, some nice crates with examples how to use them... I cannot say I've learnt a lot about async, but I got few insights on technicalities of self-learning.


+1 I enjoyed the writing style thoroughly. It made understanding the concepts super simple and fun to go through.


> I don't know! Why do they truncate stuff like that?

Because Linux limits thread names to 15 bytes (not including the required terminal nul).

Other unices are better though not great, macOS is 63 I think.

By comparison Windows allows 32… kilobytes.

Though “runtime” is probably what should be shortened here, “rt” is literally the name of the tokio features for configuring the runtime, so that would likely be more understandable than the trailing “-w”.


I've read many articles explaining Rust async and I think the best one is the async chapter of Programming Rust (2nd edition). It's not too daunting for someone new to async concepts, and yet comprehensive enough for someone curious about how Rust async works (future, waker, pinning).

Amazon link: https://www.amazon.com/dp/1492052590

Direct link to the chapter (if you subscribe to O'Reilly/Safari): https://learning.oreilly.com/library/view/programming-rust-2...


For an excellent, in depth introduction to async in Rust I recommend this post [1], part of the "Writing an OS in Rust" series.

This seems to be more of a hands on guide for the ecosystem.

[1] https://os.phil-opp.com/async-await/


Another hands on guide I really rate is Tokio's [1].

You create a very minimal Redis clone progressively by swapping out synchronous for async.

[1] https://tokio.rs/tokio/tutorial


I am surprised Rust doesn't lean into the type system more for declaring async, as I really like that way of framing it. Admittedly, I'm probably not writing ```impl Future<Output = Result<(), Report>> + 'a``` all that much in practice.

I like fasterthanlime's writeups but it does have one particular bugbear for me, that seems particularly common for Rust (and Python, to be fair) feature writeups: environment-based yak shaving because the author likes the mostly-unrelated library <X>. The very first thing that happens if I try to reproduce this code is a build error for cargo-edit, which is totally unrelated to the feature being taught, yet if I wanted to follow the writeup, I'd have to fix it.

I do really have this bugbear, particularly with Rust tutorials


> I am surprised Rust doesn't lean into the type system more for declaring async, as I really like that way of framing it. Admittedly, I'm probably not writing ```impl Future<Output = Result<(), Report>> + 'a``` all that much in practice.

Futures was stabilized before the async syntax, so this used to be the only way you could do it. But those were definitely the bad old days.

A handful of (brave) developers and library writers wrote async code like this back then when they really needed to, but the vast majority waited for async syntax to stabilize. Those state machines get ugly real fast.


> Futures was stabilized before the async syntax, so this used to be the only way you could do it.

While that's technically true (std::future was in 1.36 and async/await syntax was in 1.39), that's a very short timespan of only a few months. You are probably referring to the multiple years where futures only existed as addon libraries where their design was iterated on before the final design of std::future was settled on.


I stand by my opinion that any kind of documentation or tutorial should pass a validation step where the author does copy/paste of all their commands into a clean Docker container or VM image. If any error happens, your work is not done yet.

If you need to instruct users to install a compiler or an extra tool, so be it. It just must work as-is on a clean system (for which using Docker is easiest for me, but a clean VM or any other alternative is fine too).


Preferably the author doesn't copy and paste: preferably there's a documentation build step where everything gets run, relevant outputs get inserted into the docs, and any errors get caught.

This is how my docs for Zapatos[1] work, for instance. :)

[1] https://jawj.github.io/zapatos/


It is probably too much to ask everyone follow the same approach, so I'll be happy if people just test their own commands by hand. But your doc build step that validates code and catches errors? That's the ideal setup, for sure. Great job!


> I stand by my opinion that any kind of documentation or tutorial should pass a validation step where the author does copy/paste of all their commands into a clean Docker container or VM image. If any error happens, your work is not done yet.

Very much this. We often write scientific software with (command-line) instructions for nontechnical or sort-of-technical users and have learned from experience to not assume anything about the user's environment, or background knowledge, and to try to replicate our documentation's instructions line-by-line!


On the other hand, I really like that way of giving "best practice" advice, as it's really useful advice. You end up with a deeper understanding of futures, but also concrete advice that you can apply easily.


This article takes ages to get to futures because the writer seems to want to go on some tour of Rust crates first.


Not to worry, we also spend some time messing with strace and GDB. I see it more as "giving a working knowledge of Rust" than a distraction. Some of these crates/tools are lifechangers, I wouldn't want folks to miss out on them.

I'm sorry yodelshady has run into a build error for cargo-edit (was it OpenSSL? screw OpenSSL) but also: the more folks run into these issues and report them, the sooner they're fixed. Also! I think cargo-edit should just be adopted by the default cargo distribution because it's all-around really good.


I have to agree with the first comment: Speaking as someone who was a tech trainer and writer of course materials, tutorial, etc. for a couple of decades, if ``carge-edit`` is not core to what you're trying to teach, leave it out. Get your own desires (ego) out of the way, and stick to teaching what you need to teach. Leave the distractions/side-tracks/proselytising to a separate lesson. You think of the lesson as "Giving a Working Knowledge of Rust"? Great! Then title it accordingly and don't dress it up in "futures" camouflage -- you're doing your readers/learners a disservice.

Many, many tutorials make this mistake.


> Then title it accordingly and don't dress it up in "futures" camouflage -- you're doing your readers/learners a disservice.

In their defense, the title is "Understanding Rust futures by going way too deep", and I think advice and tangents on best practices modules is easily covered by "going way too deep".

> you're doing your readers/learners a disservice.

Not every tutorial needs to be the same, cater to the same type of person, or try to explain things in the same manner. I think the bigger disservice to readers/learners would be to homogenize tutorials into what you think is best. That might work best for a certain type of person at a certain skill level, but that doesn't mean everyone will be served well by that. And as the comments here illustrate, it's not like there's a dearth of Rust futures tutorials and explanations. There's definitely room for a rambling, informal, irreverent, meandering take (it's the only thing that kept me reading until the end, or even past the first page or two).


This isn't a tutorial, nor a training, nor a course though, this is a cave exploration. Its going down the wrong paths, multiple times on purpose. So pack your bag, get your tools in order, we may not return. I really like this style, the wrong paths/side tracks have so much to teach us (more than the right paths).


As someone who's been a technical writer (albeit for years rather than decades), I think this is often good advice, but not here. As other people have pointed out, this isn't trying to be a Futures 101 or Futures for Total Beginners. The title or opening could maybe be clearer about its scope (although I think "way too deep" at least implies the author isn't covering the topic as directly or succinctly as possible), but I think the digressions are interesting to Amos' target audience and part of his style.


I mean, we could also just let the guy write how he wants to write, since we're reading his stuff for free, and we aren't entitled to anything at all from him.

I enjoyed it, and I had never heard of cargo-edit before, and am really glad I now know about it!


You have mistaken this for a tutorial.


Tracking issue on cargo-edit here: https://github.com/rust-lang/cargo/issues/5586

I too really, really want this. If only I had the energy to work up some patches...


I only learnt about the logging and tracing crates existed from this blog post and I've already started using them, so thank you!


The Zero to Rust[1] series also has a fairly comprehensive look into these.

[1]: https://www.lpalmieri.com/posts/2020-09-27-zero-to-productio...


> we also spend some time messing with strace and GDB

Yes, and if I had learned about these a couple months ago it would have saved me ~8 hours tracking a bug down. So, thank you.


I'd say this is a good explanation, but a bad tutorial[1]. Which is fine, they're different things. But if you want it to be a tutorial it should be focused on just Rust Futures, and the other stuff should be separated out.

[1] https://documentation.divio.com/


IMO if your article is long enough that it crashes MobileSafari, it should have a table of contents. Or it should be a book chapter. It’s a really obvious criticism of all of fasterthanlime’s posts that scarcely bears repeating at this point but they’re not getting any shorter. I get that he has a lot to say but I just don’t have time to wade through all of that. What slice of Rust users are both such noobs they haven’t heard of cargo-edit and can’t Google it, but also so advanced they are interested in implementing Future manually to deepen their understanding? It’s ok to teach both things but why together?!


Ok I promise I'm going to stop replying to those comments (because as you pointed out they show up on every post), but! Rust is not the only language with a decent type system out there.

I can definitely see some ML/Haskell/Scala/etc. folks who would quickly pick up on all the Future stuff, drawing parallels with their usual language of choice, yet being completely unaware of all the nice tooling around Rust. It's one of its main selling points for me!

Re cargo-edit specifically, it also lets me stay in flow while writing these: instead of having to repeat "we'll just edit Cargo.toml, make sure to have that under [dependencies], uh this time we need a feature so the right-hand-side needs to be an object, not a string" - instead I can just copy into the article the actual commands I run on my side, and if a quick paragraph about where that subcommand comes from unblocks even one person then it's worth it.


I wouldn’t worry too much about it. You have your style, and it clearly works for the people who like your work. Everyone likes different things, and it doesn’t make sense to force homogeneity. It’s ok to cater to niches.


FWIW, I fall exactly into that niche. I've never written a line of Rust in my life but am quite competent with Haskell and Ruby and have dabbled in at least half a dozen others. Seeing everyday `cargo` use made me think of bundler and the Future/Result type signatures were easy to map onto Async/Either and the like.

So yes, it's a long article but I didn't mind that in the least. Keep it up!


Ignore the naysayers in this thread; I don't really get why people think it's ok nitpick how you've decided to present a particular topic that you publish to the world free of charge. It's... kinda rude, IMO.

I really enjoyed reading this piece, and had never heard of cargo-edit, so thank you for that.

As a Scala person, I've actually struggled with Rust's Future, because it's quite different and requires a new mental model of how things work, which I think you did a great job providing.


> What slice of Rust users are both such noobs they haven’t heard of cargo-edit and can’t Google it, but also so advanced they are interested in implementing Future manually to deepen their understanding?

My colleagues. Many people are skilled programmers jumping into obscure problems with only a baseline level of familiarity with the greater environment.


Or for example me.

cargo-edit looks useful. None of the tutorials I've skimmed have mentioned it.


> What slice of Rust users are both such noobs they haven’t heard of cargo-edit and can’t Google it, but also so advanced they are interested in implementing Future manually to deepen their understanding?

raises hand

I'm not sure how (or why) I'd google cargo-edit, because it's one of those things that you don't even think about until someone tells you about it, and then you're happy they did.

While I certainly wouldn't call myself a Rust expert, I'm no noob either, and am intensely interested in diving deep into topics like this. (Can we also dispense with the gatekeeping put-downs like "noob", please? It's unnecessary and rude.)

> It’s ok to teach both things but why together?!

Because the author felt like it, and since we're consuming his work for free, we're not entitled to tell him how to write?


What are y'all's thoughts on Async in embedded? A portion of the Rust embedded OSS community really likes it as an abstraction over interrupts, critical sections etc. I've been burned by over-complexity and coloring problems with it before, eg in web programming.

Do you think it's a good fit? Use-case-dependent? Maybe for things like RF?


There is an executor project: https://github.com/embassy-rs/embassy

I'm very excited about this for one simple stupid reason: sleep(). Awaiting a timer delay deep inside some code is gonna be amazing. With typical sync code, you basically have to split your code and queue up the next work somewhere that would be picked up by the timer interrupt… basically doing the whole state-keeping that async would do for you.


That sound like a nice advantage. I'd like to give this a shot on a future project. Its creator and I share some API design ideas, like using less typestate programming than other conventions, but I haven't used Embassy due to being cautious of Async. (The Typestate thing is a tangent; short explanation is it lets you check for misconfigured pins etc at compile time, but makes syntax significantly more verbose and harder to refactor)


We don't need it, so we don't use it. I think it's fantastic that it's possible, and I think that if you do need it, it's great to have it.


Not sure a better place to ask this, but we're getting to the point in most OSs where basically anything you want can finally be done truly async (non-blocking).

Except that DNS apparently still blocks[0], so usually things farm DNS requests off to their own blocking thread pools (the author ends up disabling DNS just so they can "prove" everything works with just a single thread)

What's so fundamentally difficult about writing an async/non-blocking DNS resolver? Is it just a lack of a real need for it?

[0] (quote: People have been trying to build asynchronous DNS resolvers for decades without success. Can it be done? Yes. Has it been done? No. ) https://gist.github.com/djspiewak/46b543800958cf61af6efa8e07...


I have found this comment on liburing[0] which clarifies some things for me. So is it because something like getaddrinfo is not just a simple operation, but a hodgepodge of stuff like "first try nscd, then read resolv.conf, then make a bunch of requests using one of many different protocols"?

I think I have a lot more to learn about DNS...

[0] https://github.com/axboe/liburing/issues/26#issuecomment-738...

> [re: implementing getaddrinfo] It's not planned because it's not a single operation but a complex beast reading files, exchanging messages, etc. IMHO, as it's not a single operation it fundamentally doesn't fit liburing, but implementing under some framework on-top would be more viable.


There is real need, and there are solutions.

NodeJS has a built-in DNS resolution API which is fully async / libuv-cooperative: https://nodejs.org/api/dns.html#dns_dns_resolve_hostname_rrt...

Other async runtimes bundle libs like c-ares to implement non-threadpool-based async DNS.

quoting the nodejs docs: "These functions are implemented quite differently than dns.lookup(). They do not use getaddrinfo(3) and they always perform a DNS query on the network. This network communication is always done asynchronously, and does not use libuv's threadpool."


On the OS level there is no difference between "sync" and "async". They become different in programming language runtimes.


Not really. mkdir(2) is definitely sync, io_uring has been called asynchronous since the start, and its predecesor has asynchronous in the name: https://lwn.net/Articles/776703/


There is a difference between blocking and non-blocking, which is what I am asking about. Language runtimes apparently cannot provide non-blocking DNS (as evidenced by this article, and the other one I linked which was on HN a week or so ago).

DNS seems like it perfectly fits the model of "give me a buffer (to hold the resolved IP), and I will wake you up when it's filled". Maybe io_uring (/ IOCP?) already can support this, and these articles are just mistaken about the current (or soon-to-be) state-of-the-art? Or is there some fundamental reason about DNS that make writing a non-blocking resolver very very difficult?

It's just very odd to me that userspace apps are creating their own blocking thread pools just to run DNS stuff, when they can do seemingly everything else with just a single thread if they wanted to.

(edited a few times, sorry if I caught someone who was mid-reply)


> There is a difference between blocking and non-blocking

Not really.

"Blocking" means "tell the scheduler to mark the process idle until operation finishes", while "non-blocking" means "don't mark the process but flip a semaphore bit when operation finishes".

The point of view of the OS the difference is just which bit to flip.


On windows especially i/o is usually async, the sync apis serialize all the i/o on the file object(even across threads).

It has weird consequences, e.g. you can't use a named pipe in 2 threads doing read, write separately(without doing protcol level coordination say in a proxy). You need to use asynchornous i/o to get it right.


Not related to async in Rust, but a 26 sec clean build time for a project with 69 dependencies and no code of its own just to get to the point where we can "do something useful" feels a little wasteful.


I'd say it's pretty unfair to take clean build time to consider doing something useful. 98% of my rust builds are incremental builds which take 10-15 seconds for a project I have been working on for 9 months full time and I think my dependency tree has easily 1000 dependencies.


True, it's not a real problem in most scenarios if incremental compile times are good. I still feel uneasy depending on so many other crates, but this seems to be a level of paranoia that others in the community don't share.

Having 1000 dependencies sounds crazy to me! If there's a bug in even one of them that affects you then there's gonna be a lot of digging to figure out the cause, and possibly multiple pull requests to get it fixed. I think my CPU would spend a bit longer than 10-15 secs on the linker step, too.


Compile/link times are definitely a discussion topic. Here's some quick tips:

  * Incremental compilation helps locally, not so much in CI, unless you save/restore the whole cache which gets large quickly and evicting the out-of-date objects is non-trivial.
  * In CI, sccache helps, but not as much as I'd like: there's a bunch of things that are non-cacheable, notably crates that pull in & compile C/C++ code (I'd trade 2 c-bindings crates against 12 Rust-only crates any day for that reason alone)
  * Splitting stuff across different crates helps parallelizing the build (and caching it better), which may be one of the reasons some projects end up having "1000 dependencies" (although I've rarely seen upwards of 600)
  * For incremental builds, linking does become the bottleneck. Full LTO is especially slow, Thin LTO is better. Switching to LLD improves thing. I'm hopeful that mold will improve things some more.
  * Re multiple PRs: thankfully crate families tend to live in the same repository, so fixing something across both tracing / tracing-subscriber could be a single PR, for example.
I wish the Rust community invested more in build caching, but even with the current state of things, there's often steps you can take to make things better.


Can you combine ccache with a sccache to make the C/C++ parts fast too?


Have you tried sccache? https://github.com/mozilla/sccache


It's his second bullet point


I rather have 1000 dependencies than re-implement all those 1000 dependencies myself, chase all the bugs that have already been ironed out in those, etc.

My time is worth more than my computer's time.


the reason for

> I still feel uneasy depending on so many other crates, but this seems to be a level of paranoia that others in the community don't share. Having 1000 dependencies sounds crazy to me! If there's a bug in even one of them that affects you then there's gonna be a lot of digging to figure out the cause

is that there is far, far more likely to be a bug in the version you write on your own to achieve the same goal than there is in a widely-observed library written by somebody who's chosen to specialize in that specific thing


In theory yes. But in practice that isn't always true. People often don't audit other modules on the assumption someone else had. Which means nobody ends up doing it. And if you end up with an ecosystem that favours more modules over fewer, you can end up with more modules than a given developer or team are willing to audit (a bit like "alarm fatigue" where if you have too many objects to check then people will inevitably just get lazy).

Just look at how many C and C++ libraries are maintained by 1 individual and have almost no 3rd party oversight to see that Rust can't automatically make the claim you made.

That all said, for anything complicated and/or directly security related, one should always check if there is a module first.


I look at it the other way around. You own any bug in your product whether it comes from a dependency or from code of your own; you have to fix the bug either way. Using a dependency doesn’t reduce your responsibility, but it does reduce the amount of code that you have to write yourself.


But if you are willing to own that responsibility then you should read the code you're importing to begin with. I know I do but I also know most people don't bother.

I do acknowledge that there will always be bugs that are identified by your users but equally if you're not auditing your dependencies first then it's hard to argue that you're not just passing off that responsibility wholesale to your users.


It's always a tradeoff whether you want to read some other code or work on something else. Rust ecosystem is not that mature so for a few libraries I had to end up rewriting the thing myself with some fixes or without some bloat. I'm writing an application level thing and I need as many utilities as possible as I do not want to write all the layers for all the abstractions that end up in my product. Then when something breaks I investigate, offer a fix, open an issue or whatever. I'm not writing something that requires too much reliability or whatever, the utility is elsewhere.


> there is far, far more likely to be a bug in the version you write on your own

In general I agree with this, but there is another relevant aspect to consider: something that I've written on my own for a particular project is also likely to be more purpose-built, and therefore simpler.


> Having 1000 dependencies sounds crazy to me!

I was thinking that too, but then I did a quick check of a few of my Java services for work, and many of them have more than 400 dependencies (most transitive, of course), with a few getting up to 600.

Obviously comparing Java and Rust is not exactly apples to apples, but I rarely care much about the number of dependencies in my Java projects, so I guess I shouldn't be too worried about Rust projects either (aside from link time, I guess, which javac doesn't have to do).


Async runtime, channels, tracing, backtraces. It does compile a lot of useful stuff ready for you to use.

It's all cached, so consider it more of a library install time.


Gaze upon our work, and rejoice:

        pub fn try_join<A, B, AR, BR, E>(a: A, b: B) -> impl Future<Output = Result<(AR, BR), E>>
    where
        A: Future<Output = Result<AR, E>>,
        B: Future<Output = Result<BR, E>>

I get why its like that, but I really wished this wasn't a typical Rust generics signature.


It's easy to read, isn't?

pub fn - public function

<...> - declaration of types,

A - type of first future,

B - type of second future,

AR - type of result of first future

BR - type of result of second future

E - type of error

So, public function try_join accepts two futures, which my return <AR>esult and <BR>esult or <E>rror, and returns future which will return tupple with (<AR>, <BR>) or <E>rror.


> It's easy to read, isn't?

    pub fn try_join<A, B, AR, BR, E>(a: A, b: B) -> impl Future<Output = Result<(AR, BR), E>>
    where
        A: Future<Output = Result<AR, E>>,
        B: Future<Output = Result<BR, E>>,
Come on... "A", "B", "E", "a", "b"??? Naming stuff like this is the standard way? I mean, if I know nothing about what the code does, and I find this kind of function, I'll start swearing right away.

Also, the function only does:

    TryJoin::Polling {
        a: State::Future(a),
        b: State::Future(b),
    }
Which is clearer and shorter. Why even writing a function signature like that? It's longer and more complicated to understand than the code itself! I don't know much Rust, but since a semicolon is missing this is just a return value, right?

So why not?

    let res = TryJoin::Polling {
        a: State::Future(fetch_thing("first")),
        b: State::Future(fetch_thing("second")),
    }.await?
PS: I don't want to criticize, and I'm not a Rust literate, I'm just amazed with its complexity.


So, there's a few things to point out here:

- The example you gave at the bottom won't really work, because enum variants aren't fully-fledged types on their own and they can't impl `Future`. Therefore, you can't `.await` them on their own. Furthermore, the point of `try_join` is to poll both futures until one fails, which you can't do with `async` syntax.

- Rust requires that you spell out all generic parameters like this at function boundaries. It's technically possible to infer types across such boundaries, but that has the potential for significant changes to the typing of a function to go entirely unnoticed. In fact, I believe some of Rust's documentation specifically calls out problems Haskell had with inferring function signatures like this.

That being said, you are correct that we could do this instead:

```pub fn try_join<FutureA, FutureB, ValueA, ValueB, Error>(future_a: FutureA, future_b: FutureB> -> impl Future<Output = Result<(ValueA, ValueB), Error> where FutureA: Future<Output = Result<ValueA, Error>>, FutureB: Future<Output = Result<ValueB, Error>>,```

That is slightly more readable.


I'm used to the convention of giving short all-caps acronym names to type parameters, so this is less readable to me, because I automatically assume that something named `Error` is a type, not a type parameter.


Oh god. I guess Rust designers have not foresee a function taking 5 parameters.

At this point, somehow, I'm starting to consider C++ templates beautiful! Perhaps a signature form like:

   template <FutureA, FutureB, ValueA, ValueB, Error>
   pub fn try_join() ... etc.
would be clearer at this point.


To be fair the full expansion of A and B are literally on line below.


> It's easy to read, isn't?

No, it's verbose and ugly. Easy to read would be something like:

  fn try_join(a:A:Future<E||AR>,b:B:Future<E||BR>) -> impl Future<E||(AR,BR)>
(Rounding to the nearest Rust-like syntax where appropriate and borrowing `type (||) = Either` from Haskell.)


I don't think this is a _typical_ signature. On the contrary, it's quite exceptional. In my experience having more than 1 type parameter in a function is quite rare in most code, unless you're building an advanced, composable library like a futures implementation.


I didn't mean "typical" as in "most Rust fn signatures are like that", obviously most functions are far simpler. Who would even claim that?

I meant "you will only encounter this level of generics complexity in Rust" (disclaimer: I don't know C++). They can't be that rare either, I have not written a lot of Rust but already encountered similar constructs multiple times. But admittedly that might be partly due to no-std requirements.


C#'s `Tuple<T1,T2,T3,T4,T5,T6,T7,TRest>` [1] would like a word!

[1[: https://docs.microsoft.com/en-us/dotnet/api/system.tuple-8?v...


Generics are about encoding supplemental bits of information in the type system. That information has to be spelled out somewhere


Yes, however in my experience you typically have to write out constraints like these a bunch of times. Refactoring a generic trait with 10 implementations is a nightmare. A struct with a couple of variations on the constraints for its impl blocks is also painful beyond like one generic param. It has to be spelled out somewhere, but then at minimum somewhere else, and in general about 3-4 more times.

I see why Haskell folks like their type level functions.


In Haskell I used to do this trick where if I wanted to neatly type annotate, but my function had some crazy signature, I'd compile the program, but passing the function into another function in an intentional incorrect way. The compiler would complain "expected A but got B". And I would simply copy B and make it the type annotation :P

Nowadays the editor would help you do that. When I'm coding Rust the editor shows the types inline in a small font, which is very helpful.


yeah, bounds written on the type decl shouldn't be repeated on the impl. the current typeck is not smart enough to do that but the ever-"nearly there we promise" replacement is. it'll land one day. they promise


I don't know any Rust, and that thing made perfect sense to me (compared to other things in Rust).

How would you have preferred it looked?


You can do:

  pub fn try_join<A: Future<Output = Result<AR, E>>, B: Future<Output = Result<BR, E>>, AR, BR, E>(a: A, b: B) -> impl Future<Output = Result<(AR, BR), E>> {


Rust truly is a better C++.


Do you have a better way to write the same constraints?

Conceivably if there was a way to not repeat the future parts.


Woah, he uses Iosevka for his mono font. :D

Edit: Whoever is downvoting, Iosevka is one of the favorites of the HN crowd, and also my own daily driver. It is worth taking for a spin if you haven't yet.


This is a pretty great article but I don't get the hate for futures not executing immediately and requiring an .await.

It's great to have pure futures-returning functions and being able to isolate side effects easily.

I find it much easier to reason with (coming from mainly JS, where everything is executed as soon as a promise is created).


Based on my understanding from the article...

Iterators change over space, Futures change over time.

In iterator, every time you call 'next' you get a value until the end when you get nothing. In Future, every time you call 'poll' you get nothing until the end when you get a value.


Haven't read this article yet, but I've enjoyed most of the previous ones from amos


As someone who has never played a pass, I have a question. Every article I see about rust says it's all very simple. this is something that can change from person to person yes, but is it like that for you? I wonder what people who play with rust think about it. (this is just a question please don't take it as an offensive comment)


"Simple" can mean many things.

For me Rust is "simple" because it has great tooling / great diagnostics, so when I get something wrong, most of the time it can tell me exactly what it is (and how to fix it!), and only occasionally does it send me down a rabbit hole.

Rust is also "simple" because it's memory-safe - if I can get something to compile (without unsafe code), then I know there's no use-after-free, double-free, buffer overflows, etc. I can concentrate my mind on the business logic (where bugs still can and do happen).

Finally, Rust is "simple" because it lets me build abstractions so I can see more clearly what I'm doing. At work we had a Go codebase full of goroutines and channels - I got tired of it and did a proof of concept in Rust: now it's just one async Stream, you can see the data flow clearly and worry about each stage of the pipeline in isolation, instead of having to jump across many different source files.

But no, it's not all very simple. There's plenty to learn, and some of the core concepts (ownership / lifetimes) are fairly hard to wrap your mind around, even if you've encountered something similar in other languages. My take is that it's worth it - you'll be very slow at first, but as you get more comfortable with it, so will you get faster.


(For context: I teach Rust since 2015)

Rust is "simple" in that it is based on a few solid principles. Data with layout, Ownership of the data and the distinction between sharing and mutability (through immutable references and mutable references). Most of the interesting API in _some_ way leans into that. So, the _basics_ of Rust are indeed quite constrained.

But that's "simple" in the sense of "Ruby is simple, everything is objects and message passing".

But then: Generics introduce a ton complexity on top, because they open a space where you are _potentially_ in a borrowed _or_ an owned situation. There's tons of tiny optimisations and protocols: for example that `Box<[u8]>` exists and turning it into `Vec<u8>` comes basically for free. There's tiny details like compiler assists that sometimes trigger and sometimes not. But even Generics can also be seen as an application/extension of the basic principles.

And then, there's tons of tooling and language that you need to know. Standard traits and features of the stdlib. Clever applications of Ownership based management for handles.

And particularly Rust is (currently) not _easy_, because the programming environment has fundamentals that are unusual to program that you cannot bypass as a beginner. That actually changes, the more those practices get known to people and individuals can teach each other rather than finding out themselves.

I think what people say when they say "Rust is simple" is that they can break down any house to a limited set of bricks. That is certainly true. But that needs a level of proficiency with the language. So, telling beginners "look at Rust, it's simple" is harmful - it's full of combinations and applications of its language core and it will take quite some time to learn breaking that down for people and figure out which tool is for which moment.


> this is something that can change from person to person yes, but is it like that for you?

In my opinion it really depends on your background.

For example, Rust uses the `i32` type as the standard integer. If you come from a C/C++ background this is probably not that weird, you probably know how many bits there are in 4 bytes off hand, there's `int32_t` standard types and you probably have experienced not wanting an integer with a variable size that depends on the platform.

If however you come from Python, Javascript, or maybe even Java, this might range from a little weird to very weird. Python only has the `int` type, no unsigned types and you might not know how many bits make up a "standard" integer, or even exactly how bits, signedness and integer sizes fit together. Java doesn't have unsigned types and doesn't have issues with sizes depending on the exact platform, so they might not understand why you would want the amount of bits right there in the type name.

This is just for the standard integer type. If you consider how many design decisions are a direct result of working on low level, high correctness required C++ code, the "simplicity" could range from completely simple to very much not simple.


IMO Rust is much easier and simpler than C++, but much harder and more complex than any managed GC language.


I don't think it's simple. I find lifetimes especially challenging. I love rust, but sometimes I am remarkably unproductive in it because the compiler is so strict. Honestly, sometimes I think I need flash cards for memorizing how to deal with particular compiler errors.


Lifetimes are an interesting illustration. They are conceptually surprisingly simple (they draw regions in time and check if one region is clearly smaller than another), but as a language feature very weird, as they are essentially a declarative language in an imperative setting. A lot people struggle with them more because they can't really _place_ the feature rather than applying it.


It's a west-coast tech industry consortium language that is a mashup of subsets of SML, C++ and Cyclone with some very strong opinions about aesthetic differences. "Simple" is not a good description. Lisps are simple. Rust, Java, C++ & friends are not. Also, the core paid promoters of rust are almost all alumni of the Ruby on Rails hype-train. You can judge for yourself if their promotion of rails was grounded in truth.


SML doesn't have typeclasses/traits, so it's fair to say "Haskell" instead.


SML has signatures. In the absence of HKT, I think it's not a fair comparison. Most popular languages have interfaces even if not by that name. Haskell, Scala and C++ are special in that you can describe an interface for interfaces. I'm sure I'm not giving due credit to others.


Rust has paid promoters?


We do not, but sometimes people who want to disparage me, Rust, or both, say stuff like this. It was said a lot more often in the earlier days.


Well, I certainly wasn't speaking about you specifically. However, did you not say that language promotion is what you do and that you quit Mozilla for not paying you enough for it here: https://steveklabnik.com/writing/thank-u-next ?


"promotion" doesn't appear in that post. My job was writing documentation. I did say that I was considering moving into evangelist/growth roles. My next job wasn't those two though, it was PM.

I guess you were trying to disparage Yehuda, then?


I mean that you are one of several. Hence the running joke is the "Rust Evangelism Strike Force" and not "that one rust guy."

An evangelist/growth focused role is exactly what a promoter is. If you spend large numbers of typical work-hours, which I'm assuming you're paid for, on social media to promote something, like Rust, or you go to conferences and events to speak publicly in promotion of something like Rust, then a "paid promoter" seems like a pretty accurate description.

Is that disparaging? Is that not what you are doing right now?


> An evangelist/growth focused role is exactly what a promoter is.

Right. move into. Because that was not a part of my job description at Mozilla. They didn't dislike the stuff I was doing, but it's not my work.

The disparagement is the implication of lying:

> You can judge for yourself if their promotion of rails was grounded in truth.


Given a 15 year retrospective, would you say that Rails lived up to the hype? If you think so, then there is nothing “disparaging” in that comment at all. If you don’t think so, then doesn’t that mean a reader should not regard such promotion with confidence? No need to be defensive.


It can be disparaging to say that someone is doing a thing because they got paid to, when they were not in fact paid to. Even when that thing, by itself, is not bad.

In this case it implies dishonest astroturfing or similar, especially when you talk about whether their statements were "grounded in truth". (Which is different from whether Ruby did well years later.)


[flagged]


> What an obnoxious style of writing.

It's good to see counter viewpoints but share what you or others would be looking for instead.

"This sucks" isn't helpful.

"This part sucks but would be awesome if it {did this}" is.


I usually can't keep my focus in an article that is so long, but the narrative style helped keep me hooked through the entire thing.


Don’t read it? Some people enjoy amos’ style, others don’t.

If that’s not your cup of tea you can just gravitate other presentation styles, it’s not like that’s in short supply.


>When deciding which article to read during their coffee break, people usually open several websites at the exact same moment, and read whichever article loads first.

>And that's a fact. You can quote me on that because, well, who's going to go and verify that? That sounds like a lot of work. Just trust me on this.

Sure, but I click away when it hurts to read.


The problem with analyzing stuff like this about Rust, is that even Rust itself has no idea how it works, or what syntax and semantics actually mean.

Rust doesn't merely lack a language standard, its reference and advanced documentation is also a very unfunny joke. You can learn just enough to have some idea of what you're doing, and then you're stuck huffing unicorn queefs because nobody including the compiler devs knows the exact rules on how syntax is supposed to work.

Magical shortcuts, syntax, and borrow checker tricks are added and removed at random with no rhyme or reason, and Rust severely punishes the programmer for wanting to know how the language actually works, since there are usually no answers, or worse, wrong answers, for them to find.

I still use Rust because the memory safety without a GC is wonderful, but please, don't even try to make it look like Rust has some official documentation or behavior. It most definitely does not, and what really disturbs me is how so many of the Rust community don't even want a standard.

So OP, everything you've just done, will be rendered obsolete and woefully wrong in a couple years when Rust serves up another opaque unicorn-burger.


>Magical shortcuts, syntax, and borrow checker tricks are added and removed at random with no rhyme or reason

Everything else aside for now, this is just patently false. There's a well-established RFC process for any such changes to the language/compiler/standard library.


every time this comes up i just have to ask

- what do you think a language standard is or does

- what languages with standards do you think standardization has helped (this is a trick question! do not answer "c" or "c++"!)

- in which languages with standards is a plurality of code written to that standard? (this is also a trick question! do not answer "c", "c++", or "javascript"!)


a "language standard" is something i can use to hold implementation developers accountable. it doesn't matter if it's an actual international standard, or just a document on the internet. I want a dry, exhaustive and systematic description of the features and intended behaviors. Refer to the Vulkan spec (https://www.khronos.org/registry/vulkan/specs/1.2/html/) to get an example of what i consider a good specification.

The rust docs don't currently meet that bar. They're quite WIP, and I imagine someone hitting a particular edge case will not necessarily be well equipped to understand whether the behavior they're observing is intended, a bug in the implementation or something else entirely.


Yes.

There is a great deal of written information about Rust-the-language which exists only in compiler comments, RFCs, bug reports and internals.rust-lang.org threads (often slightly stale in all cases).

It's Rust's greatest weakness.

But "every time this comes up" the discussion somehow gets sidetracked into arguments about standardisation, even in cases (such as this thread) when it's clear that what the ranter cares about is accurate documentation.


a "language standard" is something i can use to hold implementation and library developers accountable. I want a dry, exhaustive and systematic description of what code is legal and illegal. Instead we have arguments over whether some unsafe code found in the wild is UB and will break once LLVM fixes noalias for real this time, not UB (eg. holding raw pointers into a Vec while the Vec moves without reallocating), compiles fine but may break in the future (eg. holding raw pointers into a Box while the Box moves), compiles fine but the Stacked Borrows rules forbid it but will eventually be changed to not forbid it (Pin). Instead of a firm contract between the compiler and the programmer, we have unsafe code guidelines which is the language team's best idea at the moment for what is legal or not.

Even though C++ code in the wild is filled with standard violations, I like having a standard explaining what rules exist, so developers can reason about them, and I can use them to tell other developers to change their code because their code is invalid. (In practice I don't know if violating strict aliasing through reinterpret_cast rather than bit_cast can lead to compilation errors, and sadly std::variant can miscompile due to broken aliasing optimizations in library code even with valid user code: https://www.reddit.com/r/cpp/comments/j7gn2d/stdvariant_is_b....) Currently, not all unsafe Rust can be classified as either valid (make sure the compiler doesn't break it) or invalid (change the code until it's valid), though there are some cases where unsafe Rust is clearly invalid.

Maybe it's a temporary state while the language team makes decisions based on how the unsafe library ecosystem progresses. Maybe the language team has no plans to put the language on a firm groundwork defining exactly what is legal or not, which I find uncomfortable (though maybe others feel more comfortable with it). In any case, there's not that much practical impact on the quality of code being written and binaries being produced, but it still hurts to see people building castles on uncertain foundations.


Yeah that's a whole nother can of worms for me with Rust, something that pisses me off a lot. By enforcing strict aliasing with no way to disable, Rust is making unsafe code intrinsically more unsafe, because you can't get predictable behavior. I'd rather it be called UB and work as intended anyways. But no, rustc devs in their infinite wisdom decided not to provide what is to me a core compiler flag.


> I'd rather it be called UB and work as intended anyways.

Personally I disagree. If you want to write freely aliasing code, I think it's better to use &UnsafeCell or &Cell or &RefCell, perhaps with an Arc (or my AliasPtr crate) instead of trying to convince rustc that &mut isn't exclusive.


and to answer your other questions - Java and Python both have what I consider good, detailed specifications (which one could call "standards").


This is silly. That's like saying that speed limits don't work because everybody goes over them. Even if people don't 100% comply with standards, the fact that they exist reigns in peoples' behavior more so than if there wasn't one at all.


I assume your criticism revolves around breaking changes as what you mention is simply not true; there is plenty of documentation and language definition and I never found an issue.

Development languages evolve all the time. It's normal, the same happens to human languages.

I can't even say Rust is changing that much (especially compared to JS, my main work language).


Documentation and roadmap are the 2 make-or-break things I look for in a language/framework/tool. If one of these things is not satisfied, I have an incredibly hard time convincing myself to invest time.

I have taken a look at the Rust documentation, and I just can't see myself participating at this point because of it. The roadmap is also unclear to me. I google 'Rust Roadmap', and guess what the first result is?

https://rust.nolt.io/roadmap

So far a video game's roadmap is ranked above that of this language. I had to search for 'rust-lang roadmap' to get a good top hit... Which brought me here:

https://blog.rust-lang.org/2020/09/03/Planning-2021-Roadmap....

> The core team is beginning to think about the 2021 Roadmap, and we want to hear from the community. We’re going to be running two parallel efforts over the next several weeks: the 2020 Rust Survey, to be announced next week, and a call for blog posts.

A call for blog posts. Beginning to think about...

As a .NET developer, this honestly sounds like a dumpster fire to me. Who is actually in charge? What is the vision for 2025? How do you explain your software technology roadmap to your customers when you are using Rust and have to operate in contract lifetimes of 5+ years?

Clearly, I don't know the first goddamn thing about the Rust ecosystem or why I would want to use it. I am just a dirty .NET plebian looking in from the outside. It doesn't look very compelling right now. Can someone correct my perspective on this? I am open to it. There must be some value I am missing. Maybe Rust just isn't for the "enterprise", or whatever boring niche box I seem to be stuck living in these days.

There are obviously use cases and happy developers out there. Don't let me get in your way.


> Who is actually in charge?

https://www.rust-lang.org/ has a big old "governance" header on it, which links to this page https://www.rust-lang.org/governance

That page explains the process, who is in charge of it, and links to the roadmap for this year.

2021 is a pretty chill year for Rust overall. The project is largely focused inward, the Foundation is newly launched and figuring itself out, the edition is going to be much smaller than in 2018, and most things that are shipping are polish, bugfixes, and small API improvements.

> What is the vision for 2025?

We only plan in year increments; it is extremely difficult to do longer-term forecasting than that in an open source project. To be honest, even yearly is hard to do with more detail than high-level goals.


> What is the vision for 2025?

Why not wait and figure that out when we get there? The world is changing too fast now; consider that any vision for 2020 written in 2015 would probably have been useless by the time we were actually in 2020.

Edit: Also consider that updates to Rust probably don't have nearly as big an effect on your customers as changes to .NET, especially the old .NET Framework which was shipped as a shared runtime environment on the customers' machines. Assuming you ship binaries to your customers, Rust probably isn't even visible to them.


What? Are you sure you're looking at the right language? Or maybe you've confused stable rust and nightly rust?


No, I'm definitely on the right language. For example, lots of magical implicit reborrows with fuck all in the documentation, pattern matches and specifiers with no formal behavior defined, lints that change constantly. I do use nightly generally, simply because stable is crippled in features, but the same issues apply to stable.

I should mention that even when they adjust these things in a stable release, they never fix the documentation to reflect it. And in the case of many of these things, they aren't documented at all, except perhaps a couple sentences about their existence.


PRs welcome!


These issues cannot be fixed by merely updating the docs for a few items. (and frankly I don't contribute much to big FOSS projects because I've had bad experiences with big projects before)

This can only be fixed by A. Requiring that all syntax changes, no matter how small, are fully documented before they reach stable or even beta, and B. Eventual standardization, even if the reference implementation ends up being more cutting edge.

Rust devs also need to think more about how they're going to document a feature before they add it. Like for many of the implicit reborrows, they're done in non-obvious places and not done in others, with no pattern to where it's done and where it's not. That kind of implementation is extremely painful to document or standardize. Rust needs to think more in terms of rules for syntax, rather than instances of a particular shortcut. Rather than adding a standalone instance, you should edit the rules so that instance is covered.


Are you interested in standardization for its own sake or as a forcing function for exhaustive documentation?


Both, though mostly documentation. To me, a standard is the documentation. The fact I have to read rustc source to find syntax is frankly an abomination. A standard defines syntax and grammar in ways that a html reference page never does. It doesn't matter as much for "fluffy" languages like Python or Lua, but it's very important for a systems language.

I also want standardization to make writing alternative conforming compilers easier. I strongly dislike the attitude of some Rust maintainers, and a standard is a useful tool to forcibly remove some level of control from their hands.


While this rant is excessive, I do see some of the annoyance, for example looking at: https://doc.rust-lang.org/core/cmp/trait.PartialOrd.html

The top talks about lt, le, gt, ge, but then the docs below talk about '<' and '>', which I assume are lt and gt? But then there seems to be no requirements in le and ge? I assume everything needs to be consistent?

Also (relating to auto-ref), why does 'a<b' work, when the trait takes a reference?

EDIT: Some of this is revealed if I scroll down and look at the definitions of 'lt' and friends, but I'm already pretty confused at the top.

EDIT 2: This really wasn't worth the effort of defending, but I made an issue with my comments on PartialOrd.




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

Search: