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.
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.
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.
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".
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.
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.
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??
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).
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: the error messages! They're so readable, it's like pair programming with the borrow checker. Especially when you use the VScode integration.
Flop: 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.
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 arraysgreat 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.)
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?")
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 ).
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.
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."
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.
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.
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.
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.
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.