Small but painful annoyances when writing Rust code

I recently went back to writing Rust code, and I still get the feeling that there are a number of “papercuts” in the language: small annoyances that nevertheless make daily programming in Rust somewhat annoying and make the language feel immature.

The ones listed there are generic enough that they should be encountered when writing any type of program, with the exception of very simple programs.

Most or all of these had issues or RFC opened, but then nothing happened, despite the fact that most of them are clearly things that should already have been implemented and several are straightforward to design and implement.

In several cases (e.g. while/for loops having values), the issue has been closed due to problems in the proposal that in fact have an easy solution.

Here’s a list of them and the improvements to avoid them:

1. Non-lexical lifetimes: https://github.com/rust-lang/rust-roadmap/issues/16

This one is essential to be able to match on something, taking references to contents, and then conditionally replacing the whole thing, without introducing an extra Option variable and conditional afterwards.

This is something that has been necessary for years, and has been blocked on MIR, but now MIR is there, so it seems like implementing this should have a very high priority.

2. General “if let”: https://github.com/rust-lang/rust/issues/929

A fundamental characteristic of C-like programming languages is that “if A {if B {…}}” can be rewritten as “if A && B {…}”.

But it is not so in Rust, due to “if let” not allowing that.

It should be fixed, by making “let A = B” where A is refutable be an expression returning bool when inside an if or while conditional (possibly anywhere not in statement position). A must not contain non-_ bindings unless at the top level or inside one or more && expression from the top level.

This way, one can write “if (let A = B) && C” and “if A && let B = C” as one would expect to be able to.

3. “else match”: https://github.com/rust-lang/rfcs/issues/1712

Just an easy improvement, avoids requiring braces, immediately obvious what it means when reading code.

4. Loop return values for while/for: https://github.com/rust-lang/rfcs/issues/961

Break with value has been added for “loop” but not for “while” and “for”, which should return Some(x) if break x is executed, and None otherwise.

The compatibility issue is easily solved by making “while” and “for” return Option<T> if and only if at least only break statement with a value is present, thus preserving the current behavior if no such break statements are present.

5. A Debug impl should be required for all types: https://github.com/rust-lang/rust/issues/28185

There is no reason for a type to not implement Debug (at worst one could just print the bytes in the memory representation or even just the struct/enum name), so all types should be required to, and it should be a trait that is always considered present on a type parameter.

For backwards compatibility, just automatically inserting #[derive(Debug)] if Debug is not otherwise implemented should be fine.

6. Warn about public types not implementing obvious traits

There are crates that expose C-like enums without an Eq, Hash, Clone, etc. implementation, and similar terrible things.

There should be a warning if any “basic” trait (including serde ones) could be derived on a type but it’s not implemented or derived, with a #[derive(!Trait)] syntax to silence the warning.

7. Make default type parameters work: https://github.com/rust-lang/rust/issues/27336

Currently if you write:

enum Enum {
  Abc(usize),
  Def(T)
}

print!("{}", Enum::Abc(0)).

you get an error that rustc can’t infer a type for T.

But we specified a default for T, so it should just use it…

8. impl Trait and extensions: https://github.com/rust-lang/rfcs/issues/1522

An essential feature for returning iterators that needs to be stabilized ASAP.

And also (with lower priority) extended so that you can say for instance that two functions return the same type, for instance by allowing to name the “impl Trait” type by writing "type Foo: Trait = " in the impl.

9. Non-exhaustive enums: https://github.com/rust-lang/rust/issues/32770

There needs to be a way to add additional variants on enum without breaking compatibility due to consumers being allowed to pattern match them exhaustively.

A “…” variant syntax was suggested in an issue, but it was never implemented.

Also a small improvement for structs, as it would be nicer than using a dummy private field.

10. Default struct field values: https://github.com/rust-lang/rfcs/issues/1806

There needs to be a way to add additional fields to all-pub structs without breaking compatibility.

Allowing default values on fields (with “_” meaning to just use Default::default()) would allow that

11. Generic parameters on modules and blocks

Currently if you have several items that take the same generic parameters, you have to specify them for each one, which pollutes the code with boilerplate.

For top-level items, this can be fixed by allowing modules to take generic parameters, which would be equivalent to adding them to all the items.

For non-top-level items like fns in an impl block, a simple block with generic parameters would work.

11. Auto-coerce between T and &T if T: Copy: https://github.com/rust-lang/rust-roadmap/issues/17

When using an HashMap with integer keys, you’d expect to be able to index it with an integer, but you have to take a reference every time instead; when iterating a Vec of integers, you’d expect to be use them, but instead you need to dereference them.

Coercion would avoid the annoyance and since T: Copy semantics are not really affected.

12. Associated type constructors, for<T> bounds: https://github.com/rust-lang/rfcs/issues/1598

Required for proper expressivity.

13. Merge the lazy_static, bitflags and similar crates into libstd

They are so small and fundamental that they should just be there (or in general for lazy_static, in the same crate that implements std::sync::Once).

In general, if a crate implements something that other programming language haves in the language itself, and there is no benefit in having multiple implementations, then it should probably be in libstd.

14. Merge ArrayVec, SmallVec, OrderMap into libcollections

Fundamental enough that they should be there.

Also encourages people to actually use ArrayVec/SmallVec instead of releasing slow programs because they used Vec for small things, and encourages people to use OrderMap instead of releasing non-deterministic programs because they used HashMap.

15. Compiler should recover from missing semicolons

When missing a semicolon, the compiler doesn’t have a specific error (it should say “you probably missed a semicolon”, not "expected one of those tokens’).

Also, after printing the error it should assume the semicolon and continue compiling and showing errors for the rest of the code instead of getting confused as it does now.

16. Compiler should warn about missing braces using indentation to figure out the mismatched ones

Currently the compiler doesn’t always correctly identify the missing brace.

However, almost all source code is properly indented, and the indentation can allow to reliably find the missing brace and provide an helpful suggestion to the user instead of requiring them to go on a hunt through the code.

17. Better IDE support

It seems VS code with vscode-rust with RLS is the IDE setup one is supposed to use, but it doesn’t work that well.

When typing “.”, it should always complete; when using Ctrl+click it should always go to the definition and never fail. Currently these things only work if you get lucky.

17 Likes

Thanks so much for writing this up! I’ll make sure everything here is represented as part of the roadmap discussions around learnability and productivity.

4 Likes

I am sceptic regarding the "Loop return values for while/for", I'd like to see more use cases, and to be more sure it doesn't make Rust less readable. I think there are features more important than this one to implement.

"impl Trait and extensions" is an important feature, but it needs the right amount of time to be designed well (it could also keep in account F# computational expressions, and more).

I'd like Rust to become less bug-prone, because this is one major selling point of Rust, it's perhaps the main reason I am using Rust, and in my opinion there are still some missing parts.

This means I'd like ranged values (and enumerable enumerations?), and similar things, that improve the safety, with compiler optimizations to spot some of such bugs at compile-time, and optimize away some tests at compile-time:

If you try to implement something in Rust you use integer numbers all the time, but often they are meaningful only in a certain interval. A reliable language should allow you to express such intervals with a nice syntax.

I'd like more love and care for slices and arrays (this is not enough, it should include some slice-length value analysis):

This is not a principled solution for Rust, it's a hack and it works only for a very limited subset of this problem:

Having precise match on numbers is something I expected in Rust 1.0, but it's still missing:

In a not bug-prone language you can't ask people to write code like this:

#![feature(inclusive_range_syntax)]
fn main() {
    for i in 0u8 ... 255u8 {
        match i {
            0 ... 10 => println!("{}: small", i),
            11 ... 255 => println!("{}: big", i),
            _ => println!("{}: something else?", i),
        }
    }
}

Because the need for the "_" catch-all could introduce bugs in the code.

There are few more examples like this. Such features are not very visible, but with several other similar things already in Rust, make Rust a more reliable language.

Link for #2 should be https://github.com/rust-lang/rfcs/issues/929

#2 Link broken?

I’d especially second 1,2,3,5 and 10. And amend 10. with default function arguments.

Regarding 13:

Something that should really, really be in std as well is some variation of error_chain.

An easy way to declare wrapping error types, with backtrace support.

Nothing needs to be said here :wink:

Somehow never really had a situation where not having either of them was annoying, but still nice to have

This or automatically/implicitly assume a #[derive(Debug)] if no explicit Debug implementation is given would be realy nice to have. Not having Debug implemented for external type is super annoying but then forgetting to do so on a internal type (especially if created to a macro is quite easy).

Anyone wanting to write a RFC for it ;=)

(as a site not the case where debug can't be implemented/doesn't make much sense for some fields it could just default to something like Type { .. } being analog to omitting fields in pattern matching)

Yeees, Nooo, uh wait.

I don't like the #[derive(!Trait)] part and can't decide about the serde part. As a counter proposal why not use the existing warn/allow/etc. mechanism, i.e. have a warn_pub_no_<name>_impl warning and a way how crates can specify there own warn_pub_no_<name>_impl to have a standardized way to extend it to serde.

(And be more sensitive about the when to use it, e.g. warning about PartialEq on a type containing a non PartialEq implementing field would be annoying)

I have run into this recently too, currently it seems type inference does not use default generic parameters at all, but it sometimes would be really useful.

My case was a builder struct which had a optional but nevertheless generic parameter. If inference would use defaults by not using set_the_optional(...) it has exactly 0 specification what the generic should be from the code using it and could infer it to be the specified default. Now I have a constructor with much to many parameters and consider switching to using trait objects as the overhead in the given case should not be to hight.

But then this would probably be kinda low in implementation priority.

Having impl Trait in return position available on stable is probably the think most people a currently waiting for. All other aspects like abstype (or however it was named), using it in parameter position etc. are useful and needed for consistency but much less important, through we have to wait until it's sure that there are no compatibility problems with them.

Or generally any "this think can't be matched exhaustively" to also make the single private doc hidden field for some struct's obsolete.

Except that this might not work if the top level item does not use all the generic parameters added to it (because of inference) so you have to detect which of the generic parameters are actually used and only add them, which can be tricky to implement and might have some corner cases wrt. to stability/API breakage when adding another generic parameter in a way which, with current implementations, does not affect the API, you also might want to have default generic parameters working with inference before standardizing it.

I'm not a fan of adding thinks to std it really doesn't bring much benefit.

My person experience is that for many projects IntelliJ+Rust Plugin is currently slightly better (expect if you have to use lldb/gdb).

EDIT: And finally make arrays first class types which also feel like such, this nevertheless requires some form of integrals in the type system

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.