I’m slowly grinding through my first big Rust project and I’ve discovered that I have an axe to grind with the way lots and lots of Rust documentation works. This approach:
Upon encountering a restriction, however, it’s common to attempt to find the easiest work-around, rather than understanding the why the restriction exists in the first place.
I’ve found it a lot more useful to approach writing Rust code from the opposite direction: instead of thinking in terms of restrictions applied against the more unrestricted model that I know from C++, it’s a lot more practical and productive to think in terms of gradually building the “least unrestricted” model that enables me to support the functionality I’m looking for. Obviously, understanding how various ownership or async features work is instrumental to that.
But by now I’ve come to loathe the “standard” Rust blog post, which goes along the lines of 1. Here’s what we want to do, 2. Here’s how it looks like it should work but spoiler alert, it won’t compile, 3. Here are 1-7 more ways of doing it incorrectly 4. Here is the final version which seems to tickle the borrow checker just right.
IMHO this is the wrong way to go about it. You shouldn’t tweak code until the compiler stops complaining. You should – armed with an understanding of the underlying data model – aim for coming up with a model – and the code that implements it – which incorporates and leverages things like AxM and boxing, not code that works despite these things.
I really enjoyed your post. Maybe it’ll help turn the tide a little :-)
I’ve found it a lot more useful to approach writing Rust code from the opposite direction: instead of thinking in terms of restrictions […] it’s a lot more practical [to gradually build] the “least unrestricted” model that enables me to support the functionality I’m looking for.
I totally agree! To analogize, implementing a large Rust application is a bit like mopping a building. If you design your building with an open floor plan, mopping it is almost trivial. If you fight Rust and end up designing an Ikea, tough luck. Ikea’s a bit of a maze, and if you start in the wrong place, it’ll be easy to mop yourself into a corner.
I never thought I’d be complaining about this, but sometimes Rust compiler error messages are a bit too good. “Change this type exactly like so, put this lifetime here, you’ll be all set.” After several iterations of this process, nervously watching the error count tick down, you’ll reach a point where there’s a single error left. Depending on the design of your application, this error could be a make-or-break moment: if it points out an core tenant of Rust that your design ignores, then tough luck, try again.
The Rust compiler doesn’t necessarily care that your model is wrong; it can’t know what you’re trying to do. (At least not yet). It’ll happily tell you to mop forward a foot at a time, until you suddenly realize you’ve got walls on three sides and a mopped hallway behind you.
With this in mind, it makes sense why most Rust blog posts are structured in the manner you laid out:
[…] along the lines of 1. Here’s what we want to do, 2. Here’s how it looks like it should work but spoiler alert, it won’t compile, […] 4. Here is the final version which seems to tickle the borrow checker just right.
These posts are recounting the story of how someone mopped themselves into a corner, and then mopped themselves out of it. It’s bit like a wise janitor saying, “hey, if you ever come across a room shaped like this, here’s the best way to mop it.” These stories serve as cautionary tales, and are pretty useful when you’re in the thick of implementation work. They don’t help if you’re the one designing the building you’re about to mop!
So here’s what I’d like to see happen: I wish that blog posts about Rust—or, y’know, technical posts in general—started by explaining the underlying model, used errors and examples to illustrate how this model works, and finally discussed the designs that work best with this model so that you, the building designer, never have to contend with those errors while mopping.
Anyway, I digress. Thank you for taking the time to read the post and to leave a thoughtful response!
This part is, in my experience, the hard learning curve of rust initially. You’ll have to hit that multiple times until you remember that certain things just don’t work, or you may test them earlier to validate you mental model of the borrow checker.
Rust, at it’s core, is a programming language of restrictions. These restrictions exist for good reason.
Whether or not a restriction is a good one isn’t an objective assessment, it’s a function of the context in which it is applied. There are situations in which the restrictions enforced by Rust are good. And there are situations in which the restrictions enforced by Rust are not good. If a borrow error would cause a devastating failure in your application, then it might be wonderful that Rust prevents them at a language level! But if a borrow error would have no meaningful impact on your bottom line, then it probably doesn’t make sense to pay the costs required to prevent it.
Rust’s abstractions aim to be zero-cost. Rust pulls no punches trying to hide how things work under the hood.
Reducing the cost of an abstraction necessarily increases its surface area — it must embed fewer simplifying assumptions, and must expose more underlying details. All else equal, it produces a weaker abstraction. Sometimes that’s appropriate! Not always. Cost is one variable among many in the value equation.
I’m slowly grinding through my first big Rust project and I’ve discovered that I have an axe to grind with the way lots and lots of Rust documentation works. This approach:
I’ve found it a lot more useful to approach writing Rust code from the opposite direction: instead of thinking in terms of restrictions applied against the more unrestricted model that I know from C++, it’s a lot more practical and productive to think in terms of gradually building the “least unrestricted” model that enables me to support the functionality I’m looking for. Obviously, understanding how various ownership or async features work is instrumental to that.
But by now I’ve come to loathe the “standard” Rust blog post, which goes along the lines of 1. Here’s what we want to do, 2. Here’s how it looks like it should work but spoiler alert, it won’t compile, 3. Here are 1-7 more ways of doing it incorrectly 4. Here is the final version which seems to tickle the borrow checker just right.
IMHO this is the wrong way to go about it. You shouldn’t tweak code until the compiler stops complaining. You should – armed with an understanding of the underlying data model – aim for coming up with a model – and the code that implements it – which incorporates and leverages things like AxM and boxing, not code that works despite these things.
I really enjoyed your post. Maybe it’ll help turn the tide a little :-)
I totally agree! To analogize, implementing a large Rust application is a bit like mopping a building. If you design your building with an open floor plan, mopping it is almost trivial. If you fight Rust and end up designing an Ikea, tough luck. Ikea’s a bit of a maze, and if you start in the wrong place, it’ll be easy to mop yourself into a corner.
I never thought I’d be complaining about this, but sometimes Rust compiler error messages are a bit too good. “Change this type exactly like so, put this lifetime here, you’ll be all set.” After several iterations of this process, nervously watching the error count tick down, you’ll reach a point where there’s a single error left. Depending on the design of your application, this error could be a make-or-break moment: if it points out an core tenant of Rust that your design ignores, then tough luck, try again.
The Rust compiler doesn’t necessarily care that your model is wrong; it can’t know what you’re trying to do. (At least not yet). It’ll happily tell you to mop forward a foot at a time, until you suddenly realize you’ve got walls on three sides and a mopped hallway behind you.
With this in mind, it makes sense why most Rust blog posts are structured in the manner you laid out:
These posts are recounting the story of how someone mopped themselves into a corner, and then mopped themselves out of it. It’s bit like a wise janitor saying, “hey, if you ever come across a room shaped like this, here’s the best way to mop it.” These stories serve as cautionary tales, and are pretty useful when you’re in the thick of implementation work. They don’t help if you’re the one designing the building you’re about to mop!
So here’s what I’d like to see happen: I wish that blog posts about Rust—or, y’know, technical posts in general—started by explaining the underlying model, used errors and examples to illustrate how this model works, and finally discussed the designs that work best with this model so that you, the building designer, never have to contend with those errors while mopping.
Anyway, I digress. Thank you for taking the time to read the post and to leave a thoughtful response!
This part is, in my experience, the hard learning curve of rust initially. You’ll have to hit that multiple times until you remember that certain things just don’t work, or you may test them earlier to validate you mental model of the borrow checker.
Whether or not a restriction is a good one isn’t an objective assessment, it’s a function of the context in which it is applied. There are situations in which the restrictions enforced by Rust are good. And there are situations in which the restrictions enforced by Rust are not good. If a borrow error would cause a devastating failure in your application, then it might be wonderful that Rust prevents them at a language level! But if a borrow error would have no meaningful impact on your bottom line, then it probably doesn’t make sense to pay the costs required to prevent it.
Reducing the cost of an abstraction necessarily increases its surface area — it must embed fewer simplifying assumptions, and must expose more underlying details. All else equal, it produces a weaker abstraction. Sometimes that’s appropriate! Not always. Cost is one variable among many in the value equation.