Rust - What would be your top and flop?

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 https://github.com/rust-lang/rust/issues/53639. 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

as a beginner:

top

  • package manager and build tools
  • Option and Result
  • community
  • std
  • error messages

flop)

  • overly strict borrow checker for beginners
  • implicit returns (really hard to find that line without semicolon in a 100 line function)

Top: As someone who comes from a C++ background, rust feels like it was expressly designed to address its flaws while keeping a similar performance model:

  • memory safety: of course there is the ownership system, that reifies what I had been doing manually in C++ for years, and that was reliant on documentation to express the lifetime relationships between objects (with the possibility for bugs, or some tradeoffs with copies when the risk is too high). but memory safety in rust is so much more than that. No longer do I have to take extreme care with “rule of five” shenanigans because my class implements RAII. I no longer suffer from fields uninitialized by a buggy constructor. I don’t accidentally pass a temporary instead of a reference to a class because I typo’d and left out the & in the return type of a getter (that particular segfault was painfully stupid). Where it matters, I can mark functions and traits as unsafe to signal clients that they should look for additional preconditions in the documentation should they use them. These parts of the code where memory safety isn’t enforced by the compiler stand out with unsafe blocks.
  • compilation model: not relying on textual inclusion but having a package system allowing to add dependencies in a declarative manner is a very welcome simplification from CMake. Allows more aggressive inlining, and also use of items independently of declaration order.
  • module system: handling privacy at the module boundary rather than at the class boundary is much more flexible and comes in handy for e.g. unit tests, or for patterns where some objects are strongly coupled by design (e.g. builders)
  • option type to express the possibility of absence. I use std::experimental::optional (C++14) extensively, but the ergonomics suffers from the lack of match, and the presence of nullable pointers is redundant
  • result type comes to fix C++'s broken error story, where the ecosystem is split between exception users and non-users (the replacement being error codes, or various home-made error systems). I rolled up my own result implementation in C++, but it is home-made, suffers from the lack of std::variant in C++14, has worse ergonomics and I still have to use whatever my dependencies chose for error handling when interacting with them.
  • more generally, I miss rust rust enums all the time. I wonder why the simple concept of “can be A or B” took until C++17 to come, and only as a library type.
  • traits as interface: allows for templates that are much easier to use (not “write-only”). This pushes a style with lots of monomorphizations that is hard to achieve in C++ for various reasons (such as the compilation model) and is unified with runtime polymorphism through trait objects.
  • iterator trait that is easy and safe to implement, and come with lots of adaptors that are helpful for all iterators. No longer do I have to go through so much boilerplate while treading carefully for memory errors whenever I need to define a custom iterator
  • zero-sized types: allows to express compile time properties with zero runtime cost.
  • Generally, good defaults: memory safe by default, explicit conversions by default, immutable by default, objects movable but not copyable by default

Flop: The language is young, inertia in this space is high (billions of lines of code have been written in C++). As a result, I find myself missing some dependencies in rust (qt, an equivalent to boost::icl::interval_map), and if we wanted to integrate rust to our existing codebase, we would have to rely on FFI while our C++ codebase makes extensive use of templates and so isn’t ffi friendly. This without accounting for the cost of building the same level of expertise in rust as in C++… Also as a result of its youth, I find myself missing some features (some of which may land this year): const generics (parameterising a template by an enum), variadic templates (which we use for implementing services with compile time checked arbitrary signatures).

I don’t mean to pick on C++, I still think we can build excellent software using that language (hopefully, this is what we are doing!), but Rust just happens to fix many of the hindrances I encounter in my usage of C++.

9 Likes

Top: I do not have to worry. The compiler kicks me in the face until I get it right. I feel comfortable writing thousands-of-lines applications without having to worry about a thing, just get the damn job done.

4 Likes

Tops

Error handling is excellent. Yes, there’s a lot of it, but I see it as bringing up to the surface assumptions that are usually glossed over. Some sort of function-local way to handle common checks may be nice though, I guess.

First C & C++ rival that “gets it” (100% GC-free, bare-metal aspirations, common-sense features like closures and type inference, immutable-by-default, if let etc) and actually seems to catch on.

No silly idealistic ideas hampering the language (cough Go cough)

A relatively rich ecosystem of libraries.

Flops

Generic & lifetime syntax can get really ugly. A concise way to define and express generic and lifetime constraints, to air out the definitions a bit, would be nice: constraint MyConstraint = A + B where …;

macro_rules tangles up fragment type declarations with the invocation syntax definitions. I truly hate reading macro patterns because of this.

Case patterns

Implicit returns are only natural in one-line fns and lambdas, coding guidelines be damned. Explicit returns are required if you want an early exit, which makes the code look inconsistent once you end up using both flavors of return.

Speaking of coding guidelines being damned, I really don’t like snake case. Warning about opinionated conventions by default is too much imo. The extra underscores make the code wider than I’d like, and I read camel case just fine.

Tooling and documentation have ways to go. Last I checked, there was no comprehensive A-to-Z guide (only attempts) on everything to consider when doing C interop, only bits and pieces of info spread across ebooks, articles and stack overflow reccomendations.

Turbofish

Modules, use syntax, project file structure etc.

1 Like

Top: The borrow checker thinks about the hard stuff so I don’t have to.
Flop: The build takes too long.

1 Like

Curious, are there reasons why you consider them flops?

Top: Be explicit and the compiler checks everything else for you (except out of bound issues).

Flop: Sometimes not sure how to convert between different struct and enum without reading the whole documentation.

Beginner here (haven’t even made it all the way through the book yet)

Top: the community is great. Super helpful, lots of resources available, lots of blog posts, New Rustacean podcast.

Flop: with two very young kids and a full time job, it’s hard to find the time to put in to learn rust properly. I’ve played around with it a little, but rust is kind of like vim in this regard. It’s worth the investment, but hard to convince people to make the investment in learning it.

Random thought: I’m super interested to see if the Xi editor ever fully makes it off the ground. Given that it’s frontend agnostic, I feel like we could finally have an open source version of sublime text (aka easy to set up and use, super fast).

4 Likes

Looks like my “flops” will be a bit of an unusual opinion here, but I think expanding anonymous types (i.e. impl Trait) is one of the biggest mistakes Rust made. Lambdas were bad enough, but at least they couldn’t cross api boundaries originally. The problem with impl Trait, or anonymous types more generally, is that it is viral - once you use an anonymous type, every use forever after must also be anonymous, unless you wrap it in a boxed trait object. Anonymous types just don’t work in a language which was designed around the assumption of explicit type signatures. And instead of fixing the underlying issue, it seems like people are just piling hack onto hack.

Another issue is the choice of default integer types when values are underconstrained by type inference, combined with unchecked overflow by default. This has caused bugs in real world code I’ve worked on.

That being said, Rust remains the only issue when you want code that is both fast and correct. My top is that Rust is the first language to make systems programming practical and enjoyable.

in Cargo.toml:

[profile.release]
overflow-checks = true

(and whenever you want performance since your op cannot overflow (or is allowed to), you just use n1.wrapping_add(n2) instead of n1 + n2 and expect overflow-checks to be disabled)

use ::std::fmt::Display;

fn print_impl (x: &'_ (impl Display + ?Sized))
{
    println!("{}", x);
}

fn print_generic<T : Display + ?Sized> (x: &'_ T)
{
    print_impl(x);
}

fn print_with_added_runtime_method_resolution (x: &'_ dyn Display)
{
    print_impl(x); // or print_generic(x)
}

So impl Trait is not viral, and can always downgrade to dyn Trait when needed, and not the other way around: thus the viral thing here are trait object definitions; and since those do have a runtime cost (vtable dynamic method resolution), impl Trait seems like a far saner default than trait objects.

The one legitimate complaint here, imho, is the implicit : Sized bound on generic types, though, which makes it actually incompatible with trait objects unless it is explicitely overriden with : ?Sized.

2 Likes

The turbofish is a matter of personal taste, wrt how it looks.
As for the others, they simply don’t click as easily as I’d hope (maybe because I tend to think of imports in terms of implicitly relative paths by default but I may be wrong).

1 Like

I’m not sure why you think you are disagreeing with me, since your code example proved my point perfectly.

The issue is that as soon as you return a value with anonymous type, every subsequent use must either also use anonymous types (hence referring to them as “viral”), or wrap it in a trait object (Dyn Trait) which incurs runtime overheard, as you noted. The problem is that anonymous types are not first class citizens in Rust.

Or use it as a generic parameter, is this bad too?

Top:

The strong type system means if it compiles, it works (modulo logic bugs) and you can make invalid states unrepresentable. That means you’ll write better quality, more robust code out of the box compared to other languages, and the average crate on crates.io tends to have a higher average quality.

Flop:

Asynchronous programming. I’ve been using Rust for 2 or 3 years now, but I use C# for my day job. Rust’s way of doing async is nowhere as nice as async in C# (or JavaScript for that matter). And as @z0mbie42 mentions, the current callback hell is only amplified by Rust’s memory model and our pursuit of zero-cost abstractions.

Some further notes…

In my view, this is a key aspect of being an expression oriented language as is common in other ML-like languages. In other words, “implicit” return is really the function body (or more generally: a block) evaluating to an expression. This makes sense if you consider things like const X: u8 = { let mut x = 0; x + 1 }; We could highlight the expression oriented nature of Rust even further by allowing e.g. fn foo() -> u8 = 42;.

See trait aliases for this basic idea.

In the sense that you would like to not write the :: part of .collect::<Vec<u8>>? This is discussed in https://github.com/rust-lang/rfcs/pull/2544.

Can you elaborate on what you mean?

This is addressed by https://github.com/rust-lang/rfcs/pull/2515 which is hopefully making progress soon. The key is to give names to unnameable things by e.g. writing type Foo = impl Trait;.

Rust’s path & module system changed recently in Rust 2018. See [1] and [2]. Imports now work in a relative fashion by default thanks to “uniform_paths”. Hopefully that should make things better for you.

9 Likes

Speaking as someone else who also sees it as a wart, I am perfectly aware of the fact that the turbofish cannot be removed from Rust without introducing syntactic ambiguity. The rest of the syntax mandates its existence.

That doesn’t make it not a wart. The language could have avoided it, but that would’ve required more changes than that to still be able to distinguish chained comparators and generics. It’s a “local maxima”, since there are no nearby improvements but there are better places far away.

4 Likes