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 .