33 comments

[ 2.7 ms ] story [ 64.2 ms ] thread
This is one of the best Rust articles I've ever read. It's obviously from experience and covers a lot of _business logic_ foot guns that Rust doesn't typically protect you against without a little bit of careful coding that allows the compiler to help you.

So many rust articles are focused on people doing dark sorcery with "unsafe", and this is just normal every day api design, which is far more practical for most people.

Wow that’s amazing. The partial equality implementation is really surprising.

One question about avoiding boolean parameters, I’ve just been using structs wrapping bools. But you can’t treat them like bools… you have to index into them like wrapper.0.

Is there a way to treat the enum style replacement for bools like normal bools, or is just done with matches! Or match statements?

It’s probably not too important but if we could treat them like normal bools it’d feel nicer.

I think you can do something like impl defref but not sure that's a good idea hah. Maybe it's a different trait I'm thinking of
Loved the article, such a nice read. I am still slowly ramping up my proficiency in Rust and this gave me a lot of things to think through. I particularly enjoyed the temporary mutability pattern, very cool and didn't think about it before!
Good article, but one (very minor) nit I have is with the PizzaOrder example.

    struct PizzaOrder {
        size: PizzaSize,
        toppings: Vec<Topping>,
        crust_type: CrustType,
        ordered_at: SystemTime,
    }
The problem they want to address is partial equality when you want to compare orders but ignoring the ordered_at timestamp. To me, the problem is throwing too many unrelated concerns into one struct. Ideally instead of using destructuring to compare only the specific fields you care about, you'd decompose this into two structs:

    #[derive(PartialEq, Eq)]
    struct PizzaDetails {
        size: PizzaSize,
        toppings: Vec<Topping>,
        crust_type: CrustType,
        … // additional fields
    }

    #[derive(Eq)]
    struct PizzaOrder {
        details: PizzaDetails,
        ordered_at: SystemTime,
    }

    impl PartialEq for PizzaOrder {
        fn eq(&self, rhs: &Self) -> bool { 
            self.details == rhs.details
        }
    }
I get that this is a toy example meant to illustrate the point; there are certainly more complex cases where there's no clean boundary to split your struct across. But this should be the first tool you reach for.
While better, a person modifying PizzaDetails might or might not expect this change to affect the downstream pizza deduplication logic (wherever it got sprinkled throughout the code). They might not even know that it exists.

Ideally, imho, a struct is a dumb data holder - it is there to pass associated pieces of data together (or hold a complex unavoidable state change hidden from the user like Arc or Mutex).

All that is to say that adding a field to an existing struct and possibly populating it sparsely in some remote piece of code should not changed existing behavior.

I wonder whether there's a way to communicate to whoever makes changes to the pizza details struct that it might have unintended consequences down the line.

Should one wrap PizzaDetails with PizzaComparator? Or better even provide it as a field in PizzaOrder? Or we are running into Java-esq territory of PizzaComparatorBuilderDefaultsConstructorFactory?

Should we introduce a domain specific PizzaFlavor right under PizzaDetails that copies over relevant fields from PizzaDetails, and PizzaOrder compares two orders by constructing and comparing their flavours instead? A lot of boilerplate.. but what is being considered important to the pizza flavor is being explicitly marked.

In a prod codebase I'd annotate this code with "if change X chaange Y" pre submit hook - this constraint appears to be external to the language itself and live in the domain of "code changes over time". Protobufs successfully folded versioning into the language itself though. Protobufs also have field annotations, "{important_to_flavour=true}" field annotation would be useful here.

This made me wonder, why aren't there usually teams whose job is to keep an eye on the coding patterns used in the various codebases? Similarly like how you have an SOC team who keeps monitoring traffic patterns, or an Operations Support team who keeps monitoring health probes, KPIs, and logs, or a QA who keeps writing tests against new code, maybe there would be value to keeping track of what coding patterns develop into over the course of the lifetime of codebases?

Like whenever I read posts like this, they're always fairly anecdotal. Sometimes there will even be posts about how large refactor x unlocked new capability y. But the rationale always reads somewhat retconned (or again, anecdotal*). It seems to me that maybe such continuous meta-analysis of one's own codebases would have great potential utility?

I'd imagine automated code smell checking tools can only cover so much at least.

* I hammer on about anecdotes, but I do recognize that sentiment matters. For example, if you're planning work, if something just sounds like a lot of work, that's already going to be impactful, even if that judgement is incorrect (since that misjudgment may never come to light).

Indexing into arrays and vectors is really wise to avoid.

The same day Cloudflare had its unwrap fiasco, I found a bug in my code because of a slice that in certain cases went past the end of a vector. Switched it to use iterators and will definitely be more careful with slices and array indexes in the future.

> Cloudflare had its unwrap fiasco,

Was it a fiasco? Really? The rust unwrap call is the equivalent to C code like this:

    int result = foo(…);
    assert(result >= 0);
If that assert tripped, would you blame the assert? Of course not. Or blame C? No. If that assert tripped, it’s doing its job by telling you there’s a problem in the call to foo().

You can write buggy code in rust just like you can in any other language.

I'm not reading a solid argument as to not use "..Defaults()" because doing so suggests that you may introduce a bug and therefore should be explicit about EVERYTHING instead? Ugh. Hard disagree.
The tech industry is full of brash but lightly-seasoned people resurrecting discredited ideas for contrarianism cred and making the rest of us put down monsters we thought we'd slain a long time ago.

"Defensive programming" has multiple meanings. To the extent it means "avoid using _ as a catch-all pattern so that the compiler nags you if someone adds an enum arm you need to care about", "defensive" programming is good.

That said, I wouldn't use the word "defensive" to describe it. The term lacks precision. The above good practice ends up getting mixed up with the bad "defensive" practices of converting contract violations to runtime errors or just ignoring them entirely --- the infamous pattern in Java codebases of scrawling the following like of graffiti all over the clean lines of your codebase:

  if (someArgument == null) { 
    throw new NullPointerException("someArgument cannot be null");
  }
That's just noise. If someArgument can't be null, let the program crash.

Needed file not found? Just return ""; instead.

Negative number where input must be contractually not negative? Clamp to zero.

Program crashing because a method doesn't exist? if not: hasattr(self, "blah") return None

People use the term "defensive" to refer to code like the above. They programs that "defend" against crashes by misbehaving. These programs end up being flakier and harder to debug than programs that are "defensive" in that they continually validate their assumptions and crash if they detect a situation that should be impossible.

The term "defensive programming" has been buzzing around social media the past few weeks and it's essential that we be precise that

1) constraint verification (preferably at compile time) is good; and

2) avoidance of crashes at runtime at all costs after an error has occurred is harmful.

The Java one can actually be quite helpful, for a couple of reasons:

1. It tells you which variable is null. While I think modern Java will include that detail in the exception, that's fairly new. So if you had `a.foo(b.getBar(), c.getBaz())`, was a, b, or c null? Who knows!

2. Putting it in the constructor meant you'd get a stack trace telling you where the null value came from, while waiting until it was used made it a lot harder to track down the source.

Not applicable to all situations, but it could be genuinely helpful, and has been to me.

Nice article. The problem of multiple booleans is just one instance of a more general problem: when a function takes multiple arguments of the same type (i32, String, etc.). The newtype pattern allows you to create distinct types in such cases and enforce correctness at compile time.
In Pattern: Defensively Handle Constructors, it recommends using a nested inner module with a private Seal type. But the Seal type is unnecessary. The nested inner module is all you need, a private field `_private: ()` is only settable from within that inner module, so you don't need the extra private type.
Aside from just being immensely satisfying, these patterns of defensive programming may be a big part of how we get to quality GAI-written code at scale. Clippy (and the Rust compiler proper) can provide so much of the concrete iterative feedback an agent needs to stay on track and not gloss over mistakes.
(comment deleted)
What's really nice is where you don't need defensive programming in Rust.

If your function gets ownership of, or an exclusive reference to an object, then you know for sure that this reference, for as long as it exists, is the only one in the entire program that can access this object (across all threads, 3rd party libraries, recursion, async, whatever).

References can't be null. Smart pointers can't be null. Not merely "can't" meaning not allowed and may throw or have a dummy value, but just can't. Wherever such type exists, it's already checked (often by construction) that it's valid and can't be null.

If your object's getter lends an immutable reference to its field, then you know the field won't be mutated by the caller (unless you've intentionally allowed mutable "holes" in specific places by explicitly wrapping them in a type that grants such access in a controlled way).

If your object's getter lends a reference, then you know the caller won't keep the reference for longer than the object's lifetime. If the type is not copyable/cloneable, then you know it won't even get copied.

If you make a method that takes ownership of `self`, then you know for sure that the caller won't be able to call any more methods on this object (e.g. `connection.close(); connection.send()` won't compile, `future.then(next)` only needs to support one listener, not an arbitrary number).

If you have a type marked as non-thread safe, then its instances won't be allowed in any thread-spawning functions, and won't be possible to send through channels that cross threads, etc. This is verified globally, across all code including 3rd party libraries and dynamic callbacks, at compile time.

Article mostly focuses on code practices to avoid making logical mistakes when iterating on your program.
I fully agree with the actually great thing being what not to have to look out for and my first thought when seeing the headline was: "Doesn't the type system handle most of that stuff?"

In other languages I get most of the benefits by sticking to functional programming practices and not mutating stuff all over the place. Rust's type system sort of encodes that, and maybe a little more, by making safe mutation a known non-interfering thing.

I don’t see how your comment is relevant, none of things you mention are covered in the article. This was an article about logic bugs that can exist in spite of the borrow checker.
>Using _ as a placeholder for unused variables can lead to confusion

I would have guessed linters would have complained about what's being suggested there. Is the something special about var: _ thing that avoids it?

Defensive programming is a widely known antipattern : https://wiki.c2.com/?DefensiveProgramming

The 'defensive' nature refers to the mindset of the programmer (like when guilty people are defensive when being asked a simple question), that he isn't sure of anything in the code at any point, so he needs to constantly check every invariant.

Enterprise code is full of it, and it can quickly lead to the program becoming like 50% error handling by volume, many of the errors being impossible to trigger because the app logic is validating a condition already checked in the validation layer.

Its presence usually betrays a lack of understanding of the code structure, or even worse, a faulty or often bypassed validation layer, which makes error checking in multiple places actually necessary.

One example is validating every parameter in every call layer, as if the act of passing things around has the ability to degrade information.

The kind of defensive programming you're talking about has almost nothing to do with the contents of this article. This article is mostly just about structuring code so that bugs appear at compile time rather than runtime.
It's not about fear of "degrading information".

A function must check its arguments. It cannot assume that the arguments are already checked (against its own requirements). This is regardless of what called it, or where the values came from.

(comment deleted)
This is a great article. Anyone know more content like this?
Some of the advice is applicable tp C++ as well. Enums and such things with non-exhaustive checks all have warnings and setting warnings as errors helps.
In the first example, the match feels extremely overkill. Vec.first() exposes the correct semantic (as does Vec.iter().nth(0) for the more general case), returning an Option.
Those patterns look good.

Question: how to encourage such patterns within a team? I often find it difficult to do it during code reviews and leading to unproductive arguments about "code style" and "preferences".

Funnily, these arguments do not happen when a linter pops a warning instead...

This has already been hashed over a hundred thousand times, but there are also developer habits that we all need to defend against. One is pulling in needless crates.

Rust encourages that behavior. Sometimes rightly, but it does build a habit.

I spoke previously about how the Rust book uses the external rand create as a key example and it sets the tone for developers. I'm changing that stance somewhat since it was a decent strategic choice to have crypto packages plug-and-play. But tit still builds a habit.