I look at Rust a couple of times per year to see if it's Actually Good yet. Every time I'm very happy to see that whatever bothered me most about it last time has been improved, and then I get further into it and find new things that bother me. The last few times they've been more ergonomics issues than "no you can't do that" issues, which is cool and exciting. I happen to be in one of those cycles right now, and here's what's bothering me this time.
This is half for future research purposes (so next time I try it I can just go down this list and research each one to see what's new, without actually having to start a project) and half in case a Rust expert either gets inspired to improve it or tells me about the obvious-in-retrospect better way to do a thing.
borrow checking still complains about trivially-okay uses of closures
There's a thing I do relatively often in other languages, where I declare a local function that never leaves the outer function's scope in any sense, and is only used to name and/or reuse a block of code within that function. It's a relatively obvious pattern in languages that have any sort of closures.
For example, let's say I want to read some data in a length-prefixed format that looks like this:
let buffer: [u8; 7] = [ 2, // first byte is a total thing count 1, 2, // each thing is a length, followed by that many bytes 3, 4, 5, 6 // a multibyte thing ];
I might write this as:
fn read_things(iter: impl IntoIterator<Item = u8>) { let mut iter = iter.into_iter(); let thing_count = iter.next().unwrap() as usize; for _ in 0..thing_count { let length = iter.next().unwrap(); let mut thing: Vec<u8> = vec![]; for _ in 0..length { thing.push(iter.next().unwrap()); } println!("here's a thing: {:?}", thing); } }
This is fine, and it compiles and runs. But if I refactor that a bit, perhaps in preparation for more complexity:
fn read_things(mut iter: impl Iterator<Item = u8>) { let mut read_length = || iter.next().unwrap() as usize; let mut read_thing = || { let length = read_length(); let mut thing: Vec<u8> = vec![]; for _ in 0..length { thing.push(iter.next().unwrap()); } thing }; let thing_count = read_length(); for _ in 0..thing_count { println!("here's a thing: {:?}", read_thing()); } }
This is the same code! It could be trivially transformed into the first function by inlining all the calls to the two closures. But now Rust is no longer okay with it, because it assumes a closure is borrowing a captured value even when one can trivially prove that no call of that closure is currently on the stack (playground link):
error[E0499]: cannot borrow `iter` as mutable more than once at a time --> src/main.rs:28:26 | 26 | let mut read_length = || iter.next().unwrap() as usize; | -- ---- first borrow occurs due to use of `iter` in closure | | | first mutable borrow occurs here 27 | 28 | let mut read_thing = || { | ^^ second mutable borrow occurs here ... 32 | thing.push(iter.next().unwrap()); | ---- second borrow occurs due to use of `iter` in closure ... 37 | let thing_count = read_length(); | ----------- first borrow later used here [and a similar error for read_length]
I wish autodereferencing of copyable values happened in more places
In the above playground link's main
I had to do this:
// bad: type mismatch between u8 and &u8 read_things_ok(buffer.iter()); // ok: read_things_ok(buffer.iter().map(|i| *i));
In the real code I simplified it from, I had a similar problem with Option<u8>
:
// bad: get(i) returns Option<&u8>, not Option<u8> let (a, b): (Option<u8>, Option<u8>) = (buffer.get(0), buffer.get(1)); // ok: let (a, b): (Option<u8>, Option<u8>) = (buffer.get(0).copied(), buffer.get(1).copied());
u8
is Copy
. Option
is a trivial wrapper. I don't see why this shouldn't be an implicit copy. (I'm less convinced about the iterator one because I don't have strong intuition about how heavy an iterator is and how bad it is to implicitly add a .map()
call.)
(This is less bad when it's a bare reference and not a reference wrapped in a monad. I'd also settle for a way to say "dereference the thing inside this monad" with similar conciseness as *ref
, but I don't see an obvious way one could implement that since the language doesn't really know what a monad is. Maybe a NestedDeref
trait that backs an operator?)
I wish value
implicitly converted to Some(value)
C++:
std::optional<uint8_t> x = 1; function_taking_optional_u8(1);
Rust:
let x: Option<u8> = Some(1); function_taking_optional_u8(Some(1));
I'm mostly on board with "conversions should be explicit", but only when their presence tells you something important that you may have missed. Like, Box::new()
does a heap allocation, so maybe don't make u8
implicitly convert to Box<u8>
. This should almost certainly be opt-in per type, and rarely used. But Some
adds zero useful information here; it's just telling you "this value isn't None
", which you could already trivially tell by just looking at it.
Also, C++ having better ergonomics than you is just embarrassing.
vague struct names and import-all-the-things as a default don't interact well
One small project I'm working on right now, which consists of just a main.rs
, uses no less than four structs named Config
: embassy_stm32::Config
, embassy_stm32::i2c::Config
, embassy_stm32::usb::Config
, and embassy_usb::Config
(entirely different from the other USB config; one is hardware setup and one is USB protocol setup).
And I'm not even using all of them.

rust-analyzer
, when I refer to one of these and haven't use
d it yet, gives me the option to either import the name Config
as-is, or to use the entire fully qualified path. I would greatly appreciate it if it would also suggest importing just the submodule and referring to i2c::Config
, or adding a local import. Rust (the language) supports both of these just fine if you type them out manually, but manual use
management is tedious and rust-analyzer
has spoiled me so I really don't want to.
poor support for "it's single threaded and always will be"
Rust really wants to enable Fearless Concurrency. Sometimes I really want to just make a mutable global variable of a type that isn't thread-safe, in a single-threaded program. These two goals are at odds.
Let's say I want a process-wide cache of things for some reason.
struct Thing(); static mut THING_CACHE: HashMap<String, Thing> = HashMap::new();
error[E0015]: cannot call non-const associated function `HashMap::<String, Thing>::new` in statics --> src/main.rs:4:50 | 4 | static mut THING_CACHE: HashMap<String, Thing> = HashMap::new(); | ^^^^^^^^^^^^^^ | = note: calls in statics are limited to constant functions, tuple structs and tuple variants = note: consider wrapping this expression in `std::sync::LazyLock::new(|| ...)`
Okay, no runtime global constructors, fair enough. I don't want or need a lock, though, because this is a single threaded program.
struct Thing(); static mut THING_CACHE: LazyCell<HashMap<String, Thing>> = LazyCell::new(|| HashMap::new());
Oh good, this actually works! Let's try to use it:
THING_CACHE.insert("k".to_string(), Thing());
error[E0133]: use of mutable static is unsafe and requires unsafe block --> src/main.rs:7:5 | 7 | THING_CACHE.insert("k".to_string(), Thing()); | ^^^^^^^^^^^ use of mutable static | = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
This isn't unsafe! There aren't multiple threads! I would be happy with a "no threads allowed" project option I can set somewhere, which either (1) relaxes ownership rules intended for safe concurrency but yells at you if you ever try to spawn a new thread, or (2) just makes all mutable static variables thread-local.