Think Local, Act Local
Software projects work best when there are multiple sub-parts with lean, well-defined interfaces.
No, I’m not discussing microservices here. Instead I want to outline a number of Rust features that help us, nudge us or even force us to design our programs as a set of self-contained items.
But first, why is this of interest? After all, the compiler usually has the whole crate up front. So there’s little benefit to having e.g. functions as self-contained as possible. The real reason is that when you read a function, you can usually infer what’s going on without consulting external code much of the time. Now, this isn’t always the case (people can write spaghetti in any language), but for many code bases I’ve seen in the last two years, it works.
What does this mean for Rust programmers? The outcome is twofold: 1. They can concentrate on the function at hand, keeping their heads mostly free from detail outside the current scope. 2. the compiler will alert them if they change something that will necessitate changes elsewhere.
This is a big reason why Rust feels easier than C where you have to hold the whole program in your head to guard against harmful interference of other parts of the program. It means I can program Rust when tired or distracted (or even drunk, as one Rustacean remarked on twitter). To me, it means I can write Rust code while cradling a child. To be fair, I have already done that in C and assembly, too, but only for minuscule programs. I doubt I would be able to keep sufficient focus for bigger projects.
Enough of the why, how the merry rustacean does Rust do it?
- private and immutable by default. The less you show other modules and the less you allow them to meddle, the more control you retain over your data.
- Similarly,
Send
andSync
should only be implemented for types that are proven (this mostly works, see Ralf Jung’s take) - type inference works only within functions, so you at least have the input types to work with. Even within functions there are limits (which unfortunately make some code less ergonomic, as the type has to be supplied), for example when dealing with trait references.
- lifetime annotations strike fear in the eyes of newcomers, but once mastered allow to abstractly reason about the lifecycle of our data without having to look up everywhere
- the borrow checker ensures there is always one canonical representation of any object, getting rid of inadvertent aliasing
Note how some of this features are designed to make is easier to read and write Rust for everyone while others empower the experts at the novices’ cost.
Current RFCs follow an approach to language and API design that strives to gain the utmost from magic effects while keeping the magic itself in check. The whole process shows a lot of discipline, thought and care.
The ergonomics initiative summed the litmus tests for implicitness as follows:
Applicability. Where are you allowed to elide implied information? Is there any heads-up that this might be happening?
Power. What influence does the elided information have? Can it radically change program behavior or its types?
Context-dependence. How much of do you have to know about the rest of the code to know what is being implied, i.e. how elided details will be filled in? Is there always a clear place to look?
Now if you apply those tests to magic (even if fictional), it surprisingly makes a lot of sense. You want to know when your magic is actually going into effect. You don’t want supremely powerful magic going off unintended. And you don’t want your spells to work differently under a full moon or on Fridays with a full stomach.
So there you have it: Friendship and Language Design is magic!
The moral of the story is: Rust was and is deliberately designed to engender local boundaries to limit the stuff we need to keep in our heads at a time.