Rust - What would be your top and flop?

#1

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
#2

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
#3

@zpeters You might want to take a look at Jon Gjengset’s YouTube channel. He does videos on stream, and he’s not afraid of showing A-to-Z implementations of various stuff. Be warned, some of his videos are very very long, but well, he covers everything. I learnt a lot from them, be it on Rust and on how to solve a problem in general.

8 Likes
#4

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.

7 Likes
#5

@KillTheMule Funny to see that error handling is in your top, as far as I’m concerned I think it’s very poorly documented (no tutorial say the same thing), and the std::io::Error lack context, I think.

#6

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

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
#8

Thanks for the link. I do use failure, though it does not really help when calling a method from a crate, which can raise the same error, but for different reasons (more precisely, at different place of a same method). I wish, for instance, that ErrorKind:: NotFound would tell WHAT was not found. If the crate creator did not add context, you’re screwed, AFAIK

#9

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

1 Like
#10

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.

4 Likes
#11

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.

8 Likes
On error handling
#12

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
#13

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
#14

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
#15

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 ?

#16

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:

7 Likes
#17

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
#18

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
#19

Tops

Too many things, but to be short: I think Rust is The language to rule them all (http://www.paulgraham.com/avg.html).

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
#20

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