Rust - What would be your top and flop?

We all love Rust here, but maybe for different reasons. We all hate something about Rust, but we might not hate the same thing. So the question is simple, what's your top feature/aspect of Rust, and what's the worst feature/aspect of it ? You may only name 1, and you're, of course, allowed to comment on why it is in your top/flop.

If you have tips on how to solve someone's flop (a link to a specific doc, a crate, etc), don't hesitate...

Top: the type system, strong and expressive, I love it.
Flop: error handling, ugh. We need to work on that.

5 Likes

For me...
Top: I really feel like anything is possible if i have the skills. I could write a web app, device driver or an OS. I feel like all of the capability is there
Flop: As a beginner I get lost in all of the flexibility and options. I don't know exactly what i'd want to fix this. Maybe an "opinionated" guide to building an app from start to finish. Like you mentioned about error handling, every resource i could find had a slightly different take on how to do this.

7 Likes

Top: Error handling. First language that provides a way I'm really comfortable with.

Flop: The "old" syntax that makes it somewhat hard to see if one's using a trait object. Easily solvable by #![deny(bare_trait_object)] though.

8 Likes

Never had any problem with the docs, the book suffices imho (and is written very well), but I've also not found a huge variance in tutorials (if you don't know it yet, look at this one). I don't directly use std::io::Error, but you might want to look at the failure crate which provides a way to add context.

1 Like

TOP:

  • Global ecosystem (Cargo, crates.io,rustup, clippy, rustfmt) just efficient, clear, easy to understand
  • Compile everywhere including wasm
  • Fast & compact binary
  • Good community
  • Good documentation
  • I spend time to make it compile but I have very few bug compare to other language I am using
  • I don't find limit for now
    FLOP
  • I fill lifetime syntax ugly

Today I have only 3 months of Rust in my fingers but I am very enthusiast.

1 Like

Alright then.
Top: The intuitiveness of rust. You have an Option<T> and want to get T? .unwrap() or match it! Same with Result<T, E>. Want the answer to √2? (2.).sqrt().
Flop: The non-intuitiveness of rust. You want to pass around complicated references? Study them like it's the day before an exam ... then proceed to forget them after it "works".

2 Likes

Top: error handling. Explicitness and flexibility of error codes. Usage nearly as terse as exceptions.

Flop: the struct literal syntax should have used C99 syntax.

5 Likes

Complete Rust newbie here, just got interested in it a week or two ago and am finding it really interesting. I've been learning Haskell very gradually over the past few years and it feels like Rust has some of the nice type-system guarantees of ML-flavoured languages, without requiring beginners to learn some of the more mind-bending abstractions that can be offputting to newcomers (i.e. catamorphisms and anamorphisms, functors, monads and purity, etc).

Top (so far): Error-handling, really good type inference, and of course performance.
[edit] Oh, one thing I should explicitly call out as a big top is the use of an Option enum instead of allowing nulls. I use Scala at work and the inclusion of null is a bummer, although I guess there's no choice since it's a JVM language.

Flop: Clean compile times can be slow, making CI difficult without a caching setup. A bigger problem for me is the gigantic target directory for non-trivial apps. I built the simplest "hello world" web service with actix-web and the target directory was about 1 GB and required about 5 minutes to build on my laptop. I hope this can improve.

9 Likes

Since a number of people have mentioned error handling as either a top or a flop, I thought it was worth giving the discussion its own topic to try to work out why opinions seem to be divided.

1 Like

Top: Type level lifetimes! They may give me confidence that my super cool zero-copy algorithm is correct, but furthermore, they also tell me why when it's wrong. Lifetimes take an abstract, kind of hand-wavy idea, and reify it into a set of simple rules that can be understood with only local reasoning. What's not to love??

Flop: #[derive] is blatantly broken and can't be fixed because it would break backwards compatibility. :sob:

2 Likes

Top: Rusts portability and Memory- and Threadsafety. For me as an embedded developer these are the most important things. It is what is bothering me about C and C++.

Flop: No manufacturer support (yet). I.e. there is no SDK written in and for Rust (although there are rust-wrappers).

1 Like

Top: the infamous borrow checker; I have been waiting for such a concept since long ago and love it in practice
Flop: I really wanted non-lexical lifetimes, but now that they're there nothing really stands out; async maybe ?

Top: :grin: the error messages! They're so readable, it's like pair programming with the borrow checker. Especially when you use the VScode integration.

Flop: :sweat: explicit lifetimes? As a newbie the compiler will tell me to make lifetimes explicit to solve a problem, but that is never the actually solution. Usually it's a sign that I have a design bug -- but the compiler doesn't know that!

Tangent: I've heard that Racket allows one to explicitly enable subsets of the language, which helps newbies learn because the compiler won't suggest advanced solutions. I wonder if Rust could do something like that with the more complicated features to make the learning curve less steep. :thinking:

9 Likes

Since I'm definitely very very pleased with Rust, I'll start with some minor flops before ending on very positive tops:

Flops

Code readability
  • negating conditions (bools) with the unary negation prefix operator ! is not readable enough, imho:

    • solution: add to core either a not!(condition) macro or a not(condition) function:

      #[inline]
      fn not (condition: bool) -> bool
      {
          core::ops::Not::not(condition)
      }
      
  • &self and &mut self notation contradicts pattern matching notation, leading to counter intuitive errors (when !Copy), or worse, needless Copying:

    #[derive(Clone, Copy)]
    struct BigArray([u8; 4096]);
    
    impl BigArray {
        fn my_eq (
            &self,
            &other: &BigArray,
        ) -> bool
        {
            self.0[..] == other.0[..]
        }
    }
            
    fn main ()
    {
        let foo = BigArray([0; 4096]);
        assert!(foo.my_eq(&foo)); // wops, 4096 bytes are copied !?
    }
    
Lifetime for beginners
  • bad lifetime naming habits omnipresent in the docs (a.k.a. "everything is 'a"), are leading to the following error appearing in these forums far too often:

    #[derive(Default)]
    struct Strings<'a> (
        Vec<&'a String>
    );
    
    impl<'a> Strings<'a> {
        fn add (self: &'a mut Self, s: &'a String) // should be &'_ mut Self
        {
            self.0.push(s);
        }
    }
    
    fn main ()
    {
        let mut strings = Strings::default();
        let hello = String::from("hello");
        let world = String::from("world");
        strings.add(&hello);
        strings.add(&world);
    }
    
    error[E0499]: cannot borrow `strings` as mutable more than once at a time
      --> src/main.rs:19:9
       |
    18 |         strings.add(&hello);
       |         ------- first mutable borrow occurs here
    19 |         strings.add(&world);
       |         ^^^^^^^
       |         |
       |         second mutable borrow occurs here
       |         first borrow later used here
    
  • implicitness of lifetime ellision, specially as lifetime type parameters, lead to non beginner-friendly bugs. There is a dire need for a feature that would expand the compiler lifetime ellision choices, and ideally, it would be automatically used in case of borrowck compilation error:

    struct OneString /* = */ (&'static str);
    
    impl OneString {
        fn read (self: &Self) -> &str { self.0 }
    }
    
    fn main ()
    {
        let mut string = OneString("hi");
        let old = string.read();
        string.0 = "bye";
        println!("{}", old);
    }
    
    error[E0506]: cannot assign to `string.0` because it is borrowed
      --> src/main.rs:11:9
       |
    10 |         let old = string.read();
       |                   ------ borrow of `string.0` occurs here
    11 |         string.0 = "bye";
       |         ^^^^^^^^^^^^^^^^ assignment to borrowed `string.0` occurs here
    12 |         println!("{}", old);
       |                        --- borrow later used here
    
  • static muts are unsound "with high probability" given the &'static _ immutable guarantees forbidding any ulterior (even without overlap!) &'static mut creation;

    • on the same vein, there should be a warning in NonNull's documentation against using both impl From<&'_ T> for NonNull and unsafe { non_null.as_mut() } EDIT: it's being fixed
  • And lastly, and maybe even the most important to me: make arrays great again 1-st class citizens by having them implement IntoIterator and TryFromIterator!

Tops

  • we can express very rich invariants at the type-level, the most famous one being lifetimes, of course, but also statically dispatched closures; NonNull invariants that can, on top of that, be combined with Option<NonNull>-like layout optimizations; type-level enums (like ByteOrder), and even more to come with GATs and Specialization;

  • unsafe separation while remaining accessible, for better control of performance vs peace of mind;

  • enums and pattern-matching!! for great ADTs

  • C-level performance;

    • and sometimes even better than C: &str > char *, impl Fn() > void (*) (void)
  • (EDIT) great error messages! (except when lifetime ellision is involved)

  • and more importantly: great community and awesome ecosystem (tooling, etc.)

6 Likes

You're not wrong about this -- and lifetimes are my "top"! The error messages are typically really good, but the suggestions are just the compiler's best guess at what you meant to do, and it often guesses wrong.

What has usually worked for me is to skip over the compiler's suggestion, focus on the error message and try to understand why the thing I was trying to do is wrong. Sometimes if I am being incredibly dense that day and can't figure it out, I will try the compiler's suggestion and analyze the new error messages. But you have to understand what's wrong before you can fix it. Lifetimes are hard, and it does take brainpower -- but it's far easier in Rust ("sorry, you can't put that reference there") compared to something like C++ ("I just called the copy constructor while you weren't looking; that's the same thing, right?")

3 Likes

Tops

Too many things, but to be short: I think Rust is The language to rule them all (Beating the Averages).

Flops

  • Current async ergonomics (callback hell which is amplified by Rust memory model :sob:).
  • Crates.io: The lack of namespacing on crates.io is a real pain, and for me a real limit of rust adoption among enterprise users (security, licensing, branding...).
  • Growing complexity/fragmentation of the language. 6 weeks releases is IMO very fast paced and introduce a lot of changes in the language that you may not acknowledge if you are busy working on real life problems with Rust and then you encounter some codebases with alien syntax because of the language changes.
  • Two parallel ecosystems: stable and nightly. What's more frustrating than looking for a lib, finding the pearl, but then being unable to use it because it's nightly only. Furthermore I think this ecosystem fragmentation is really bad for Rust adoption.
  • Slow and resource intensive compilation: it's not very pleasant to see your laptop battery melting in few hours because of a resource intensive compiler.
1 Like

Top:

Rust was designed by very smart people, who put a lot of thought into it. Blocks that evaluate to the last expression are a good idea. Move semantics by default are a great idea. Stealing type classes from Haskell and adapting them to create a sensible form of template metaprogramming is awesome. And borrowck is just flat-out Galaxy Brain territory.

Flop:

Rust was designed by very smart people, and most of their choices make sense after you think about it. Of course if let has to be a completely different construct than if, since let isn't an expression, and let can't be an expression because expressions don't introduce new variables. Of course you can't return a DST, since that would require functions to alloca() into their parent's stack frame, which is not possible with a pure stack data structure. Of course for <'a, 'b> Fn(&'a T, &'b U) -> &'a usize is a thing, since you have to be able to declare a type for fn foo<'a, 'b>(&'a T, &'b U) -> &'a usize, and if the angle brackets were attached to the Fn, then you'd never be able to stablize direct use of the Fn trait. And don't even get me started on the turbofish.

But they are confusing, and they feel arbitrary because the only reason they are the way they are is because of other design choices. The confusing corner-cases surrounding if let could have been avoided by allowing expressions to introduce locals, so let would actually be an expression. The turbofish, and the weird-looking for<> syntax, could have been avoided by making other changes to the syntax.

The hard stuff is Rust's Top. Rust's big Flop is that it also includes confusing stuff, which, as Joe Armstrong put it, "you have to explain to people over and over again."

8 Likes

Hey folks. I am Mazdak "Centril" Farrokhzad from Rust's language team (but opinions here are my own!). Thank you for your inputs! I'll take the opportunity to speak to some of the issues raised here

This will be phased out soon. I expect we will first make it into a warning, then deny, and finally an error in the next edition. It does take time and we want to make sure there are no bugs. :wink:

Many on the team agree that Foo { bar: baz } is not optimal syntax in hindsight. In particular it sub-optimally composes with type ascription. Given a redo I think the Haskell syntax Foo { bar = baz } would have been neat. (Foo { .bar = baz } is decent as well). Sadly, the churn would be too much here even with an edition.

Oh yeah; this is pretty sad... Perhaps it is fixable with an edition and with other trait system advancements.

If this is recognized as a problem then adding Not to prelude would let you write .not() everywhere.

This is being discussed in Consider deprecation of UB-happy `static mut` · Issue #53639 · rust-lang/rust · GitHub. Many agree that the current situation is sub-optimal and that something needs to be done here.

Actually... I'm working on removing hir::ExprKind::If entirely and replacing it with the HIR equivalent of match cond { true => then, _ => els }. Sadly, the discrepancy in drop order in the constructs means that if cond { then } else { els } actually is equivalent to match { let _tmp = cond; _tmp } { true => then, _ => els }. I consider this a language design bug and there's no technical reason why if let couldn't behave like if or we could have match behave like if for that matter.

Moreover, I am going to introduce hir::ExprKind::Let which moves let-bindings towards actual expressions so that you can write if p && let q = r && ... { ... } and so on. Also, just because let does introduce variables does not mean it cannot be an expression. In particular, you can have (let pat = expr): bool work. The result of the expression depends on whether expr matched pat. Flow analysis then determines whether bindings introduced are definitely initialized where accessed.

We hope that the marketing around the edition should help a bit here. In particular, the edition guide is meant as a way to catch up on recent changes.

18 Likes

Top:

  • It's the best programming language I have worked with in the last 25 years
  • The base selling points really are unique: performance and memory safety with zero cost abstractions...
  • strict types are actually awesome. I had forgotten about that a bit with all the scripting languages these days. You can convey so much meaning with the type system. (downside: dealing with info only available at runtime can be challenging, and I havn't really found any great ressources about that)

Flop: It still feels very unpolished at the moment, that can be fixed, but Rust will be nearing it's 10th birthday, so I think it's about time. Some of the things:

  • manual lifetimes, I feel like the compiler should be able to know better than I how long something will live, but I admit, it's easier said than implemented.
  • diagnostics needs some love: there is 788 open issues about diagnostics, stuff like:
    • suggesting you to use unexisting or private modules or traits,
    • suggesting other syntax that will just not compile,
    • not distinguishing types with the same name but from different crates (or versions of crates)
    • ... and some 700 more.
      Note that this is one of the crucial aspects of rust that beginners need to get a good experience.
  • rustdoc needs some love:
    • the html pages it generates are really slow even though it could be an area to show off rusts wasm capabilities...
    • theming it is next to impossible right now, so we still don't ship with great themes like ayu
    • the rustdoc documentation is kind of sparse, it won't go over all the features, it won't even tell you how to make cross references
    • ...

I have the impression that diagnostics and rustdoc are two areas where we miss developer ressources in order to catch up.

  • Crucial features take a long time to stabilize
    • async
    • specialization
    • NLL
    • chalkification

I understand and really appreciate that people are working very hard to make rust better, but it does add friction when using the language, and it does so for a long time. I'm a bit afraid of rust maybe needing a complete redesign at some point, you get:

  • async fn, but not as trait fn
  • impl Trait, but only in this or that context
  • associated types, but not if they need to be tied to the lifetime of self

It feels like each of these features adds exponential complexity of how they interact with all the other features. It becomes harder and harder (and thus longer and longer) to just add one thing. For me it smells like a scalability problem. I havn't looked at the rust internals enough, but if I find that adding a feature to my software becomes a headache, I know it's time for a rewrite. With a project the scale of rust and the number of people that depend on it, that might become a real problem, but from my experience, the sooner you rewrite, the better.

3 Likes

Just so you know...

This is a design decision.

Try nightly Rust, most of this has been implemented and fixed.

There just aren't that many features right now; cross-references are not a stable feature that exists.

1 Like