What's changed since 2015 is that we ironed out some of the wrinkles in the language (non-lexical lifetimes, async) but the fundamental mental model shift required to think in terms of ownership is still a hurdle that trips up newcomers.
A good way to get people comfortable with the semantics of the language before the borrow checker is to encourage them to clone() strings and structs for a bit, even if the resulting code is not performant.
Once they dip their toes into threading and async, Arc<Lock<T>> is their friend, and interior mutability gives them some fun distractions while they absorb the more difficult concepts.
Great post! It's got a ton of advice for being productive, and it should be especially useful for beginners.
Languages I liked, I liked immediately. I didn’t need to climb a mountain first.
To each his own, I guess….
Almost 90% of the Rust I write these days is async. I avoid non-async / blocking libraries where possible.
I think this whole issue is overblown.
When it came time for me to undo all the async-trait library hack stuff I wrote after the feature landed in stable, I realized I wasn't really held back by not having it.
I very rarely have to care about future pinning, mostly just to call the pin macro when working with streams sometimes.
[0]: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
I don't know about C#, but at least in Rust, one reason is that normal (non-async) functions have the property that they will run until they return, they panic, or the program terminates. I.e. once you enter a function it will run to completion unless it runs "forever" or something unusual happens. This is not the case with async functions -- the code calling the async function can just drop the future it corresponds to, causing it to disappear into the ether and never be polled again.
I’m not calling this the pinnacle of async design, but it’s extremely familiar and is pretty good now. I also prefer to write as much async as possible.
A flat learning curve means you never learn anything :-\
In point of fact, I think the intended chart of the idiom is effort (y axis) to reach a given degree of mastery (x axis)
- another think coming -> another thing coming
- couldn't care less -> could care less
- the proof of the pudding is in the eating -> the proof is in the pudding
It's usually not useful to try to determine the meaning of the phrases on the right because they don't have any. What does it mean for proof to be in a pudding for example?
The idiom itself is fine, it's just a black box that compares learning something hard to climbing a mountain. But learning curves are real things that are still used daily so I just thought it was funny to talk as if a flat one was desirable.
People (colloquially) use phrases like "steep learning curve" because they imagine learning curve is something you climb up, a.k.a. a hill.
Most explanations of ownership in Rust are far too wordy. See [1]. The core concepts are mostly there, but hidden under all the examples.
- Each data object in Rust has exactly one owner.
- Ownership can be transferred in ways that preserve the one-owner rule.
- If you need multiple ownership, the real owner has to be a reference-counted cell.
Those cells can be cloned (duplicated.)
- If the owner goes away, so do the things it owns.
- You can borrow access to a data object using a reference.
- There's a big distinction between owning and referencing.
- References can be passed around and stored, but cannot outlive the object.
(That would be a "dangling pointer" error).
- This is strictly enforced at compile time by the borrow checker.
That explains the model. Once that's understood, all the details can be tied back to those rules.[1] https://doc.rust-lang.org/book/ch04-01-what-is-ownership.htm...
But if you come from Javascript or Python or Go, where all this is automated, it's very strange.
All the jargon definitely distracted me from grasping that simple core concept.
Rust also has the “single mutable reference” rule. If you have a mutable reference to a variable, you can be sure nobody else has one at the same time. (And the value itself won’t be mutated).
Mechanically, every variable can be in one of 3 modes:
1. Directly editable (x = 5)
2. Have a single mutable reference (let y = &mut x)
3. Have an arbitrary number of immutable references (let y = &x; let z = &x).
The compiler can always tell which mode any particular variable is in, so it can prove you aren’t violating this constraint.
If you think in terms of C, the “single mutable reference” rule is rust’s way to make sure it can slap noalias on every variable in your program.
This is something that would be great to see in rust IDEs. Wherever my cursor is, it’d be nice to color code all variables in scope based on what mode they’re in at that point in time.
Frankly most of the complexity you're complaining about stems from attempts to specify exactly what magic the borrow checker can prove correct and which incantations it can't.
Bonus: do it with no heap allocation. This actually makes it easier because you basically don’t deal with lifetimes. You just have a state object that you pass to your input system, then your guest cpu system, then your renderer, and repeat.
And I mean… look just how incredibly well a match expression works for opcode handling: https://github.com/ablakey/chip8/blob/15ce094a1d9de314862abb...
My second (and final) rust project was a gameboy emulator that basically worked the same way.
But one of the best things about learning by writing an emulator is that there’s enough repetition you begin looking for abstractions and learn about macros and such, all out of self discovery and necessity.
It has a built in coach: the borrow checker!
Borrow checker wouldn't get off my damn case - errors after errors - so I gave in. I allowed it to teach me - compile error by compile error - the proper way to do a threadsafe shared-memory ringbuffer. I was convinced I knew. I didn't. C and C++ lack ownership semantics so their compilers can't coach you.
Everyone should learn Rust. You never know what you'll discover about yourself.
It's an abstraction and convenience to avoid fiddling with registers and memory and that at the lowest level.
Everyone might enjoy their computation platform of their choice in their own way. No need to require one way nor another. You might feel all fired up about a particular high level language that you think abstracts and deploys in a way you think is right. Not everyone does.
You don't need a programming language to discover yourself. If you become fixated on a particular language or paradigm then there is a good chance you have lost sight of how to deal with what needs dealing with.
You are simply stroking your tools, instead of using them properly.
My gut feeling says that there's a fair bit of Stockholm Syndrome involved in the attachments people form with Rust.
You could see similar behavioral issues with C++ back in the days, but Rust takes it to another level.
I think that it's happened to some degree for almost every computer programming language for a whiles now - first was the C guys enamoured with their NOT Pascal/Fortran/ASM, then came the C++ guys, then Java, Perl, PHP, Python, Ruby, Javascript/Node, Go, and now Rust.
The vibe coding people seem to be the ones that are usurping Rust's fan boi noise at the moment - every other blog is telling people how great the tool is, or how terrible it is.
Side note: Stack allocation is faster to execute as there's a higher probability of it being cached.
Here is a free book for a C++ to Rust explanation. https://vnduongthanhtung.gitbooks.io/migrate-from-c-to-rust/...
Why RAII then?
> C++ to Rust explanation
I've seen this one. It is very newbie oriented, filled with trivial examples and doesn't even have Rust refs to C++ smart pointers comparison table.
>Why RAII then?
Their quote is probably better rephrased as _being explicit and making the programmer make decisions when the compiler's decision might impact safety_
Implicit conversion between primitives may impact the safety of your application. Implicit memory management and initialization is something the compiler can do safely and is central to Rust's safety story.
However, for high-performance systems software specifically, objects often have intrinsically ambiguous ownership and lifetimes that are only resolvable at runtime. Rust has a pretty rigid view of such things. In these cases C++ is much more ergonomic because objects with these properties are essentially outside the Rust model.
In my own mental model, Rust is what Java maybe should have been. It makes too many compromises for low-level systems code such that it has poor ergonomics for that use case.
The compiler knows the returned reference must be tied to one of the incoming references (since you cannot return a reference to something created within the function, and all inputs are references, the output must therefore be referencing the input). But the compiler can’t know which reference the result comes from unless you tell it.
Theoretically it could tell by introspecting the function body, but the compiler only works on signatures, so the annotation must be added to the function signature to let it determine the expected lifetime of the returned reference.
To make a compiler automatically handle all of the cases like that, you will need to do an extensive static analysis, which would make compiling take forever.
Disclaimer: I haven't taken the time to learn Rust so maybe don't take this too seriously..
Note I’m not being critical of the author here. I think it’s lovely to turn your passion into trying to help others learn.
I think it is a very good example of why "design by committee" is good. The "Rust Committee" has done a fantastic job
Thank you
They say a camel is a horse designed by a committee (https://en.wiktionary.org/wiki/a_camel_is_a_horse_designed_b...)
Yes:
* Goes twice as far as a horse
* On half the food and a quarter the water of a horse
* Carries twice as much as a horse
Yes, I like design by committee. I have been on some very good, and some very bad committees, but there is nothing like the power of a good committee
Thank you Rust!
Weather the ferocious storm
You will find, true bliss
At that point you might as well be writing Java or Go or whatever though. GC runtimes tend actually to be significantly faster for this kind of code, since they can avoid all those copies by sharing the underlying resource. By the same logic, you can always refactor the performance-critical stuff via your FFI of choice.
Yes the borrow checker is central to Rust, but there are other features to the language that people _also_ need to learn and explore to be productive. Some of these features may attract them to Rust (like pattern matching / traits / etc.)
dmitrygr•3h ago
Why would I pair-program with someone who doesn’t understand doubly-linked lists?
dwattttt•3h ago
mre•3h ago
It is doable, just not as easy as in other languages because a production-grade linked-list is unsafe because Rust's ownership model fundamentally conflicts with the doubly-linked structure. Each node in a doubly-linked list needs to point to both its next and previous nodes, but Rust's ownership rules don't easily allow for multiple owners of the same data or circular references.
You can implement one in safe Rust using Rc<RefCell<Node>> (reference counting with interior mutability), but that adds runtime overhead and isn't as performant. Or you can use raw pointers with unsafe code, which is what most production implementations do, including the standard library's LinkedList.
https://rust-unofficial.github.io/too-many-lists/
Animats•2h ago
I've discussed this with some of the Rust devs. The trouble is traits. You'd need to know if a trait function could borrow one of its parameters, or something referenced by one of its parameters. This requires analysis that can't be done until after generics have been expanded. Or a lot more attributes on trait parameters. This is a lot of heavy machinery to solve a minor problem.
umanwizard•39m ago
In practice, it really doesn't. The difficulty of implementing doubly linked lists has not stopped people from productively writing millions of lines of Rust in the real world. Most programmers spend less than 0.1% of their time reimplementing linked data structures; rust is pretty useful for the other 99.9%.
worik•19m ago
Stop!
If you are using a doubly linked list you (probably) do not have to, or want to.
There is almost no case where you need to traverse a list in both directions (do you want a tree?)
A doubly linked list wastes memory with the back links that you do not need.
A singly linked list is trivial to reason about: There is this node and the rest. A doubly linked list more than doubles that cognitive load.
Think! Spend time carefully reasoning about the data structures you are using. You will not need that complicated, wasteful, doubly linked list
dmitrygr•10m ago
But you might need to remove a given element that you have a pointer to in O(1), which a singly linked list will not do
pornel•3h ago
Trying to construct permanent data structures using non-owning references is a very common novice mistake in Rust. It's similar to how users coming from GC languages may expect pointers to local variables to stay valid forever, even after leaving the scope/function.
Just like in C you need to know when malloc is necessary, in Rust you need to know when self-contained/owning types are necessary.
mplanchard•1h ago
An example: parsing a cookie header to get cookie names and values.
In that case, I settled on storing indexes indicating the ranges of each key and value instead of string slices, but it’s obviously a bit more error prone and hard to read. Benchmarking showed this to be almost twice as fast as cloning the values out into owned strings, so it was worth it, given it is in a hot path.
I do wish it were easier though. I know there are ways around this with Pin, but it’s very confusing IMO, and still you have to work with pointers rather than just having a &str.
lmm•32m ago
Ar-Curunir•7m ago