Things you thought were awful but changed your mind about

I found my first Rust projects recently and looking at them made me remember that I had a very different view of the language in the beginning. I had obstinately told myself I would never start doing certain things that I do a lot of these days.

What are some things that you found off-putting when you started using Rust, that you later came around to liking, or even consider "must have" nowadays?

I got the impression that all those foo.iter().map(..).filter(..).collect() things were mostly FP folks trying to impress non-FP programmers with their snazzy "ability-to-chain-together-operations"-skills. A few years back there was a discussion about adapter chains vs for-loops with ifs, and someone remarked that it requires less cognitive load to parse those adapter chains, which I chuckled at at the time.

As I've been trying to force myself to use more idiomatic Rust features, I got into a phase where I forced myself to use iterators and adapters more, and I went through, roughly, the following evolution of thought:

  1. "Wait, I can't abort this chain once it has begun? This is so stupid."
  2. "Surely this must be a problem everyone else encounters? How come this is used so much if it has this obvious problem?"
  3. "Oh, I see -- they make sure that the conditions are set up so at the point the chain is expressed, things are infallible.".
  4. "Oh, oh, oh! That means that one wouldn't need all those weird ways to break out of loops!"
  5. "Ah, I see -- I've been doing iteration wrong all these years, including pre-Rust.".

I've even come around to agreeing with that it is indeed easier to read/parse, once you've memorized what all the adapters do. (This is all with the caveat that there are plenty of cases where a simple for loop is the right tool).

I also used to see newtypes as an ugly hack which was only a workaround for a flaw in the language -- a view I no longer hold.

16 Likes

There are basically only two things I didn't fully get used to in Rust:

  1. I still can't accept match ergonomics with a quiet conscience (the difference between values and references is just too important for me to be swept under the rug);
  2. the names of usize and isize, which I wish were uint and int (IIRC they used to be).

Before really knowing Rust, I've had a couple other, very minor aversions to some idioms, which turned out to be based on misunderstanding. For example, I found it weird that you can't iterate by-ref through a container without saying .iter() (or .iter_mut()), but that of course isn't true, since one can just do for item in &container or for item in &mut container.

I never found iterator adapters problematic; I actually find it quite funny that people coming from procedural OO languages with long method chains are usually the ones that complain that iterator adapters are long-winded and merely an FP show-off, while they (apparently) have no problem reading the same number of chained method calls if its about a

BeanBuilderFactory.createBuilder()
    .setName("foo")
    .setId(1337)
    .build()
    .writeTo(connection);

or something.

15 Likes

The borrow checker :rofl:. My first non-toy project involved a lot of slice iteration and mutation where speed is critical; I need something like a sliding &mut [T] and ended up writing some definitely UB iterators to get around the errors. Even worse, it happened to work, as far as I could ever tell anyway. A typical "fighting the borrow checker", "this works in C" phase. [1]

More accurately, my appreciation of "No unsafe :arrow_right: No UB" [2] eventually (far) surpassed any borrow checker gripes I have, plus now I recognize I didn't actually understand UB in depth to begin with. I also now consider "if it compiles right, it's good" to be a horrible, horrible crutch (in any language with UB). I guess this could be phrased, I consider the cultural recognition and rejection of unsoundness and UB-but-works-today a "must have".

More generally, Rust is slowly chipping away at a lot of my premature optimization tendencies. The potential bounds checks and all of that is (a) often automatically amortized and (b) usually worth it anyway. As a corollary, I'm trusting modern compilers more.

Shout out to Learn Rust the Dangerous Way.


  1. Hmmm, I've since rewritten it with split_mut and the like (with no loss of performance), but now that GAT is stabilized...... ↩ī¸Ž

  2. or a buggy dependency ↩ī¸Ž

15 Likes

Used to try and use callbacks in Rust. Now I try to use channels in other languages!

Another is trying a bit to hard to avoid cells and boxes (including mutex and arc): it's a smell, but sometimes the thing you want actually is sharing and interior mutability, and you're generally better off spending your time thinking about where it should be and not how to avoid it completely at the cost of twenty extra mut refs...

Still trying to get used to the idea that there's a package repository that doesn't suck!

My current weakness is trying to use async where it's not quite ready yet (hopefully GATs mean the async trait case is going to be addressed soon at least)

10 Likes

I am positively surprised that the optional semicolon works well.

Optional semicolons in other languages have a terrible reputation, but Rust uses semicolons differently, and the strong type system prevents worst gotchas, so overall it's fine.

9 Likes

Not from when I was new to Rust, but from when async/await was stabilized - suffix .await to await the completion of a Future.

I then worked on a mixed Python asyncio and Tokio codebase, and realised that whenever someone was doing something "clever", thing.await was clearer about what was actually being awaited than await thing - especially when thing is a complex set of join and select combinators.

17 Likes

Thank you; seeing comments like this really makes me happy. As one of the people pushing for postfix and who read through all the thousands of comments about the syntax, I'm glad this has been a fruitful use of weirdness budget.

26 Likes

Another is trying a bit to hard to avoid cells and boxes (including mutex and arc): it's a smell, but sometimes the thing you want actually is sharing and interior mutability, and you're generally better off spending your time thinking about where it should be and not how to avoid it completely at the cost of twenty extra mut refs...

So much this. I am coming more from OO, GC languages, and I kept tripping over the borrow checker and lifetimes. I learned that it was better for me to just Box things and make it work, and then I started to understand how ownership worked and how I could make it work. But trying to do the most powerful way Rust would let me was keeping me stuck in the mud. Put another way, I had to learn how to see the corners before I could cut them*

*: this is not meant to imply that references and lifetimes are bad, but that they are more efficient than using Boxes everywhere

2 Likes

I was first horrified by Option and Result but I quickly came to love it. Especially Result is such an ingenious way to just eliminate the entire exception control flow beeing separate from normal program flow.
Also I had worked before with Dart and found it really nice to have non-nullable types there. Until I learned of Rusts Option which just eliminates Null altogether without giving up on the concept.

9 Likes

This is true, but my point was more that sometimes it's not actually a cut corner to use Box or RefCell, etc: fairly often they are actually the correct, simplest option!

Try to think about them less as workarounds and more just understand what they are and why they exist: there's not much in std that is never the best option (Though that sounds like it's own fun thread!)

6 Likes

From my early days, the big thing I learnt is that Mutex, RefCell, Arc::clone and Rc::clone aren't actually that expensive once the optimizer has had a good run over my code. Hence, I learnt that if the borrow checker is fighting you, the best thing to do is to reach for shared ownership and runtime borrow checking (just as I'd reach for std::shared_ptr in C++), and remove (in priority order) Mutex, RefCell, Arc and Rc (replacing with borrows) later, if I can.

2 Likes

Wouldn't have flown in the particular case I mentioned, but Cell probably would have. I imagine it's common if your introduction to Rust is "rewrite these things that were in a scripting language before". (By the time I was doing that, I'd seen enough to not consider it awful.)

1 Like

Back to the original topic:

When I first started reading about ?, I was concerned. It sounded like throwing a lot of return statements all over the place, which I usually don't like. And I had grown to like e.g. Scala and how it was conventional to just map all over your Option/Result types, and just return them at the end of your function, rather than try to unwrap them.

But in practice, I haven't found it to be a big problem. The fact that you can't use ? unless you are specifically returning an Option/Result makes it feel like I'm still using the full power of those types. So it hasn't bothered me as much as I thought it would.

I guess what I'm saying is that

fn foo(a: isize) -> Result<isize, Box<dyn Error>> {
    let b = f1(a)?;
    let c = f2(b)?;
    let d = f3(c)?;
    Ok(d)
}

feels pretty close to

fn foo(a: isize) -> Result<isize, Box<dyn Error>> {
  f1(a)
    .and_then(f2)
    .and_then(f3)
}
1 Like

I found the lack of any "abbreviated" generic syntax extremely annoying (and still do to an extent, but understand better why it's there. Also since then rust added the impl trait syntax in parameter types which is essentially what I wanted (you still can't access any type parameters of those parameters though, which is a shame)

I found having to write impl<T> Whatever<T> for SomethingElse pretty annoying.

I also didn't like the iterator chaining, and to some extent still don't. I don't really like the builder pattern either and would prefer real named parameters, but it's a minor thing. Given how much rust programs use iterator chaining I was a bit disappointed not to find something like raku/perl6's "whatever star" syntax for making lambda functions.

I almost immediately hit problems where I expected to be able to write a "lending" iterator, where I expected "Send" to be a "specialization trait", and where I expected to be able to specialize From.

Additionally, I found working with large buffers to be extremely awkward, mostly due to the poor documentation for stuff like std::io::BorrowedBuf and MaybeUninit, I guess these are "advanced" topics but they are kinda critical to writing code that does efficient IO. On this note I also found the edges around object construction pretty sharp, and banged into them dealing with initializing Box<[u8]> buffers, better documentation for how to use the safe uninitialized box constructors allow correct codegen would have been nice. I get that this is all to prove things are initialized, but when dealing with large buffers you really, really don't want to do that by writing "twice")

Oh, yeah, I also initially didn't like local variables being const by default, because I think doesn't add that much safety and it implies the language has top-level-const which I think is a horrible idea. I was wrong about that though! rust only has top level const for locals and function parameters, and because of the aliasing rules with references you don't need to prohibit taking the address of function parameters to enable automatically passing them by reference.

2 Likes

You don't need d. This is the equivalent to your second code snippet:

fn foo(a: isize) -> Result<isize, Box<dyn Error>> {
    let b = f1(a)?;
    let c = f2(b)?;
    f3(c)
}
1 Like

Not quite, actually. ? does a From::from conversion on the errors, so if f3 returns Result<_, SpecificError> then the version that just returns f3(c) directly might not compile.

I'm a big fan of just using the version with d, because the consistency and easy editing -- if you needed to add an e, for example -- is worth more than being a bit shorter to me.

10 Likes

Yeas, I thought about that, but isn't that the case as well when using the and_then method? Or does this provide the implicit From::from conversion too?

Rust async.

At first, it felt verbose, clunky, and had "weird" constraints. Now, it feels brilliant and as if the team made the right calls / tradeoffs on all the decisions.

2 Likes

I came from C land, with a history of working on codebases that broke when we updated the compiler or switched to a different implementation (implying lots of UB hidden in there, since Clang versus GCC shouldn't have been such a big hit). Most of my problems boiled down to trying to play fast and loose with references in the way that the C codebases I worked on played fast and loose with pointers (relying on the pointer never actually dangling).

If I'd come from scripting land, I might have been happier with the idea of just wrapping everything in Arc, Mutex, Cell etc.

5 Likes

I ragequit the Rust book first time around because of shadowing. It just felt like a terrible design that could easily lead to a bug. But the ownership model together with the "unused variable" warning makes it really hard to misuse variables, even when you have multiple ones with the same name.

I even prefer it now for "nested function calls", for example:

let position = (5, 8);
let position = rotate(position, angle);
let position = add_ofset(position, ofset);

instead of: let position = add_ofset(rotate((5, 8), angle), ofset);

The second "wtf" moment for me was seeing that Rc::clone() doesn't actually clone the value, even though it is literally named "clone". But the Clone trait isn't about deep cloning, which something that stops being weird once you get used to it.

Lastly, the .await syntax was really weird at first, but it didn't take long until I needed to put several awaits in a chain and damn, this is so much better than javascript:

let result = await (await (await foo()).buz()).bar(); // JS
let result = foo().await.buz().await.bar().await; // Rust
6 Likes