Rust - What would be your top and flop?

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 @skerkour 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 Make the turbofish syntax redundant by varkor Ā· Pull Request #2544 Ā· rust-lang/rfcs Ā· GitHub.

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

Ignoring existing constraints, I would have preferred it the other way around:

let x = ....collect() :: Vec<u8>;.

It would feel less nested (And Haskell does something similar iirc) and the () would not be so far from the function name. Or maybe have some entirely new syntax that instead of the collect call will be something that puts the new collection front-and-center: ....=>Vec;
Also, seconding notriddle, it may be a necessary wart, but it's still a wart.

this looks like type ascription:
....collect() : Vec<u8>

1 Like
  • Top: Rustdoc tests.
  • Flop: The Range and RangeInclusive types.

It's essentially RFC 2522.

1 Like