Live data from Hacker News

Patterns for Defensive Programming in Rust

corrode.dev

41–50 of 99 posts

Re: Patterns for Defensive Programming in Rust

#41
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.

Re: Patterns for Defensive Programming in Rust

#42

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…

I work one of these teams! At my company (~300 engineers), we have tech debt teams for both frontend and backend. I’m on the backend team.

We do the work that’s too large in scope for other teams to handle, and clearly documenting and enforcing best practices is one component of that. Part of that is maintaining a comprehensive linting suite, and the other part is writing documentation and educating developers. We also maintain core libraries and APIs, so if we notice many teams are doing the same thing in different ways, we’ll sit down and figure out what we can build that’ll accommodate most use cases.

Re: Patterns for Defensive Programming in Rust

#44

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 go…

[deleted]

Re: Patterns for Defensive Programming in Rust

#45

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 go…

I think the "if something bad happens throw an exception" thing does have some value, namely that you can make it very explicit in the code that this is a use case that you can't handle, and not that you merely forgot something or wrote a bug.

In PHP a pattern I often employ is:

  match ($value) {
    static::VALUE_1 => ..., 
    static::VALUE_2 => ..., 
    default => static::unreachable() 
  } 

Where unreachable is literally just:

  static function unreachable() { 
    throw new Exception('Unreachable'); 
  } 

Now, we don't actually need the default match arm. If we just leave it off entirely, and someone passes in something we can't match, it'll throw a PHP error about unmatched cases.

But what I've found is that if I do that, then other programmers go in later and just add in the case to the match statement so it runs. Which, of course, breaks other stuff down stream, because it's not a valid value we can actually use. Or worse: they add a default match arm that doesn't work! Just so the PHP interpreter doesn't complain.

But with this, now the reader knows "the person who wrote this considered what happens when something bad is passed in, and decided we cant handle it. There's probably a good reason for that". So they don't touch it.

Now, PHP has unique challenges because it's so dynamic. If someone passes in the wrong thing we might end up coercing null to zero and messing up calculations, or we might end up truncating a float or something. Ideally we prevent this with enums, but enums are a pain in the ass to write because of autoloading semantics (I don't want to write a whole new file for just a few cases)

Re: Patterns for Defensive Programming in Rust

#46
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.

Re: Patterns for Defensive Programming in Rust

#47
post #11

Good article, but one (very minor) nit I have is with the PizzaOrder example. struct PizzaOrder { size: PizzaSize, toppings: Vec , 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 t…

Decomposing things just to have different equality notions doesn't generalize. How would you decompose a character string so that you could have a case-insensitive versus sensitive comparison? :)

> How would you decompose a character string

With a capitalization bit mask of course!

And you can speed up full equality comparisons with a quick cap equality check first.

(That is the how. The when is probably "never". :)

Re: Patterns for Defensive Programming in Rust

#49

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…

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.

Re: Patterns for Defensive Programming in Rust

#50

Earlier quoted context omitted.

Decomposing things just to have different equality notions doesn't generalize. How would you decompose a character string so that you could have a case-insensitive versus sensitive comparison? :)

> How would you decompose a character string With a capitalization bit mask of course! And you can speed up full equality comparisons with a quick cap equality check first. (That is the how. The when is probably "never". :)

Don't forget to store the locale used for capitalization, too.
Post reply on HN