Rust 2021 Roadmap Wishlist

2020-09-24

About

The core team sent out a call for blog posts to inspire the roadmap for the 2021 edition.  Originally, I wasn't going to write a post because I'm a college student with too much homework, but I decided to make time anyway, and write up my opinion. 

Match Statements

Match statements are my favorite feature of Rust (by far). Doesn't mean they can't be improved though. Someone who is unfamiliar with Rust might write:

fn main() {
    let mut a = 4;
    match 5 {
        a => unreachable!(),
        b => println!("{}", b),
    }
}

This code panics (which I still get confused about, despite having written a lot of Rust). You need to use an if inside of the match:

fn main() {
    let mut a = 4;
    match 5 {
        x if x == a => unreachable!(),
        b => println!("{}", b),
    }
}

Which is clear and easy to read, but the first code chunk is a perfectly logical guess as to how these statements work. There is syntactic ambiguity in the Rust language that causes the compiler to not help you. Declaring a local identifier and matching on a literal or pre-existing identifier (excluding local variables) uses the exact same syntax. And this is not the only situation where it causes problems. Take the following example:

enum KeyboardKey {
    Escape,
    F1,
    F2,
    F3,
    // …
}

use KeyboardKey::*;
match key {
    F1 => { /* … */ }
    F2 => { /* … */ }
    F3 => { /* … */ }
    Escap => { /* … */ } // Forgot the 'e' on Escape, now this is a catch-all
    k => { /* … */ }     // and this is unreachable
}

It drives me crazy that this compiles. I make this mistake all of the time. I know it prints warnings, but if you're at a point in the development process where you too many warnings to look through, it's hard to pinpoint why your code doesn't work. And, looking at it, it may not be clear what the issue is.

Instead, an edition-breaking-change should be made - to create a local identifier or catch-all you must use let (being consistent with the rest of the language, besides function parameters). I would write this code instead (And the compiler would throw an error that Escap is not the name of an identifier in scope):

/* … */

use KeyboardKey::*;
match key {
    F1 => { /* … */ }
    F2 => { /* … */ }
    F3 => { /* … */ }
    Escap => { /* … */ }
    let k => { /* … */ }
}

Now there is a syntactical difference between the second-to-last and last match arms. This makes it easy for the compiler to throw an error when the code is wrong, and I'd argue it's easier to read. I think this change should follow the same path as the dyn keyword. Not using dyn is deprecated in the 2018 edition, and will likely turn into an error in the 2021 edition. Not using let in this situation should be deprecated in the 2021 edition and turned into an error in the 2024 edition. Semantic differences like this should have their own syntax, and I think this follows the same mentality set by if let statements. A more complicated example might be:

let a: u32 = /* … */;
match a {
    let x if x.count_ones() == 1 => { /* … */ }
    0 => { /* … */ }
    let non_power_of_two_or_zero => { /* … */ }
}

What an match statement that should really be turned into an if let statement would look like:

match option {
    // `let` would be inserted here.
    let Some(value) => { /* use `value` identifier */ }
    None => { /* … */ }
}

Bonus

I would also appreciate not having to add use statements immediately above all my match statements. But, I consider what I've outlined above more important to actually solving a real problem with match statements.

enum Enum {
    A,
    B,
}

let var: Enum = /* … */;

// An implicit `use Enum::*;` for the match statement
match var {
    A => { /* except that A and B are out of scope here */ }
    B => { /* and here */ }
}

#[macro_use]

#[macro_use] should be completely removed from the language. Macros should follow the exact same import scheme as other public items. It's too confusing having multiple ways to do it, and it's obvious that Rust is moving in this direction (after the 2018 edition changes), so let's get it over with!

as

Similary, the as keyword should be completely removed from the language. Enums without associated data, and assigned numeric values should automatically implement Into and TryFrom for their #[repr] types. This would make dealing with enum and integer conversions insanely easier in many situations.

Async Closures

I would like to be able to pass async closures as literal parameters.

some_function(async |/* … */| { /* … */ });

Features

Features is a poorly named feature. But that's the least of it's problems. There is no standard way to document features. General good practice now is to make sure your features are additive, but it's not enforced at all. Generally, they are also undiscoverable - they don't show up on crates.io or docs.rs. Usually, I have to go to a crates source code and read their Cargo.toml to see what is even an option, and than guess as to what it does. I think features should be deprecated. They only have two uses: reduce compile time/binary size, and choose an implementation. The reducing compile time/binary size thing should be fixed by some mechanism to avoid compiling and including unused code (if that's possible). Choosing an implementation is interesting here, because features are additive, so they're kind of abused into an almost C-style enum. Additionally, different crates can request incompatible features (even when additive, because which implementation should be compiled?). I propose that instead, we have a choice mechanism.

lib.rs:

/// Choose either iterative or recursive implementation.
#[choice]
static enum ALGORITHM_CHOICE: AlgorithmChoice {
    /// Solve recursively
    Recursive,
    /// Solve iteratively
    Iterative,
}

/// Greatest Common Divisor.
pub fn gcd(mut a: u32, mut b: u32) -> u32 {
    match ALGORITHM_CHOICE {
        AlgorithmChoice::Recursive => {
            if a == 0 { 
                return b;
            } else if b == 0 {
                return a;
            }
            let new_b = a % b;
            a = b;
            if new_b == 0 {
                a
            } else {
                gcd(a, new_b)
            }
        }
        AlgorithmChoice::Iterative => {
            if a == 0 {
                return b;
            } else if b == 0 {
                return a;
            }
            loop {
                a %= b;
                if a == 0 {
                    return b;
                }
                b %= a;
                if b == 0 {
                    return a;
                }
            }
        }
    }
}

This would show up on docs.rs in it's own section. And if the crate is included in the dependency tree more than once with different choices, then it should compile the function separately for each choice - that way crates can't interfere with other crate's dependencies as a sibling node in the dependency tree (which they currently can with features).

Error Handling

I don't think this is much of a problem in it's current state, and I know there's some talk about adding some syntactic sugar. But, we could probably all be fine with a macro in the core library:

sum!(NewError, ErrorA, ErrorB);
sum!(CombinedError, NewError, ErrorC);

where all types are enums. Expanding to something like:

#[sum]
enum NewError {
    ErrorA(ErrorA),
    ErrorB(ErrorB),
}

#[sum]
enum CombinedError {
    ErrorA(ErrorA),
    ErrorB(ErrorB),
    ErrorC(ErrorC),
}

This would likely be a fancy proc macro, using the #[sum] attribute to allow un-nesting functionality. This is the only thing I feel I've been missing from error handling in Rust (without error crates as dependencies), but I might be alone on this one. Other proposals I've seen have been quite complicated, and I think the language is complicated enough already.

Const Generics

It would be nice to be able to provide an API as I've wanted to do before:

StackVec<4, T>::new();

Crates.io

I think crates.io should borrow some cool features from lib.rs. Although, I think the most popular libraries may not always be what's best for your specific use case (as lib.rs implies). I think it would make life easier for all Rust programmers if there was a page on crates.io that helps you find what dependency you need. I don't think I have the answer on how to accomplish this, but I think even a graphical form of awesome-rust on the home page of crates.io would be a huge improvement.

Single-threaded executor

The standard library should include a function to simply run a future on the current thread. I wrote something similar for my pasts crate.

Standard SIMD

The standard library should include safe types like u32x4 that work on all supported architectures.

Target Support

I want to see AVR support be pushed to stable, so I can do some side projects! Why? Because as a college student up to this point I have received 5 free arduinos (2 megas, 2 unos and a pololu-raspberry pi hat thing that is arduino-compatible), and I would like to use them as a way to get into more embedded development in Rust.

Conclusion

I don't think it's necessary to get all of these things out for Rust 2021, but I think for edition-breaking-changes should be made to allow these to be added in minor version updates in the future. Thanks for reading!

Feel free to email me any corrections in grammar or spelling at jeronlau@plopgrizzly.com .