Live data from Hacker News

Patterns for Defensive Programming in Rust

corrode.dev

91–99 of 99 posts

Re: Patterns for Defensive Programming in Rust

#91
post #75
post #58

Earlier quoted context omitted.

Haskell’s fromJust, and similar partial functions like head, are as dangerous as Rust’s unwrap. The difference is only in the failure mode. Rust panics, whereas Haskell throws a runtime exception. You might think that the Haskell behavior is “safer” in some sense, but there’s a huge gotcha: exceptions in pure code are the mortal enemy of lazy evaluation. Lazy evaluation means that an exception can occur after the cat…

I forgot about fromJust. On the other hand, fromJust is shunned by practically everybody writing Haskell. `unwrap` doesn't have the same status. I also understand why. Rust wanted to be more appealing, not too restrictive while Haskell doesn't care about attracting developers.

It's not just fromJust, there many other partial functions, and they all have the same issue, such as head, tail, init, last, read, foldl1, maximum, minimum, etc.

It's an overstatement to say that these are "shunned by practically everybody". They're commonly used in scenarios where the author is confident that the failure condition can't happen due to e.g. a prior test or guard, or that failure can be reliably caught. For example, you can catch a `read` exception reliably in IO. They're also commonly used in GHCi or other interactive environments.

I disagree that the Rust perspective on unwrap is significantly different. Perhaps for beginning programmers in the language? But the same is often true of Haskell. Anyone with some experience should understand the risks of these functions, and if they don't, they'll eventually learn the hard way.

One pattern in Rust that may mislead beginners is that unwrap is often used on things like builders in example docs. The logic here is that if you're building some critical piece of infra that the rest of the program depends on, then if it fails to build the program is toast anyway, so letting it panic can make sense. These examples are also typically scenarios where builder failure is unusual. In that case, it's the author's choice whether to handle failure or just let it panic.

Re: Patterns for Defensive Programming in Rust

#92
post #74
post #64

Earlier quoted context omitted.

> The point is Rust provides more safety guarantees than C. But unwrap is an escape hatch Nope. Rust never makes any guarantees that code is panic-free. Quite the opposite. Rust crashes in more circumstances than C code does. For example, indexing past the end of an array is undefined behaviour in C. But if you try that in rust, your program will detect it and crash immediately. More broadly, safe rust exists to prev…

I never said Rust makes guarantees that code is panic-free. I said that Rust provides more safety guarantees than C. The Result type is one of them because you have to handle the error case explicitly. If you don't use unwrap. Also, when I say safety guarantees, I'm not talking about safe rust. I'm talking about Rust features that prevent bugs, like the borrow checker, types like Result and many others.

Ah thanks for the clarification. That wasn’t clear to me reading your comment.

You’re right that rust forces you to explicitly decide what to do with Result::Err. But that’s exactly what we see here. .unwrap() is handling the error case explicitly. It says “if this is an error, crash the program. Otherwise give me the value”. It’s a very useful function that was used correctly here. And it functioned correctly by crashing the program.

I don’t see the problem in this code, beyond it not giving a good error message as it crashed. As the old joke goes, “Task failed successfully.”

Re: Patterns for Defensive Programming in Rust

#93
post #78
post #63

Earlier quoted context omitted.

I'm sure lots of bystanders are surprised to learn what .unwrap() does. But reading the post, I didn't get the impression that anyone at cloudflare was confused by unwrap's behaviour. If you read the postmortem, they talk in depth about what the issue really was - which from memory is that their software statically allocated room for 20 rules or something. And their database query unexpected returned more than 20 ite…

Looking at that unwrap as a Result handler, the arguable issue with the code was the lack of informative explanation in the unexpected case. Panicking from the ill-defined state was desired behaviour, but explicit is always better. The argument to the contrary is that reading the error out-load showed “the config initializer failing to return a valid configuration”. A panic trace saying “config init failed” is a mino…

First, again, that’s not the issue. The bug was in their database code. Could this codebase be improved with error messages? Yes for sure. But that wouldn’t have prevented the outage.

Almost every codebase I’ve ever worked in, in every programming language, could use better human readable error messages. But they’re notoriously hard to figure out ahead of time. You can only write good error messages for error cases you’ve thought through. And most error cases only become apparent when you stub your toe on them for real. Then you wonder how you missed it in the first place.

In any case, this sort of thing has nothing to do with rust.

Re: Patterns for Defensive Programming in Rust

#94
post #58
post #54

Earlier quoted context omitted.

The point is Rust provides more safety guarantees than C. But unwrap is an escape hatch, one that can blow up in your face. If they had taken the Haskell route and not provide unwrap at all, this wouldn't have happened.

Haskell’s fromJust, and similar partial functions like head, are as dangerous as Rust’s unwrap. The difference is only in the failure mode. Rust panics, whereas Haskell throws a runtime exception. You might think that the Haskell behavior is “safer” in some sense, but there’s a huge gotcha: exceptions in pure code are the mortal enemy of lazy evaluation. Lazy evaluation means that an exception can occur after the cat…

> The difference is only in the failure mode. Rust panics, whereas Haskell throws a runtime exception.

Fun facts: Rust’s default panic handler also throws a runtime exception just like C++ and other languages. Rust also has catch blocks (std::panic::catch_unwind). But its rarely used. By convention, panicking in rust is typically used for unrecoverable errors, when the program should probably crash. And Result is used when you expect something to be fallable - like parsing user input.

You see catch_unwind in the unit test runner. (That’s why a failing test case doesn’t stop other unit tests running). And in web servers to return 50x. You can opt out of this behaviour with panic=abort in Cargo.toml, which also makes rust binaries a bit smaller.

Re: Patterns for Defensive Programming in Rust

#95
post #39

Earlier quoted context omitted.

Is there an industry standard name for these teams that I somehow missed then?

Not exactly the question you asked, but you may want to read the chapter on “Large-Scale Changes” in the “Software Engineering at Google” book: https://abseil.io/resources/swe-book/html/ch22.html

Thanks, I shall take a look.

Re: Patterns for Defensive Programming in Rust

#96
post #94
post #58

Earlier quoted context omitted.

Haskell’s fromJust, and similar partial functions like head, are as dangerous as Rust’s unwrap. The difference is only in the failure mode. Rust panics, whereas Haskell throws a runtime exception. You might think that the Haskell behavior is “safer” in some sense, but there’s a huge gotcha: exceptions in pure code are the mortal enemy of lazy evaluation. Lazy evaluation means that an exception can occur after the cat…

> The difference is only in the failure mode. Rust panics, whereas Haskell throws a runtime exception. Fun facts: Rust’s default panic handler also throws a runtime exception just like C++ and other languages. Rust also has catch blocks (std::panic::catch_unwind). But its rarely used. By convention, panicking in rust is typically used for unrecoverable errors, when the program should probably crash. And Result is use…

The difference is not just convention. You mentioned some similarities between Rust panics and C++ exceptions, but there are some important differences. If you tried to write Rust code that used panics and catch_unwind as a general exception mechanism, you’d soon run into those differences, and find out why Rust code isn’t written that way.

The key difference is that in the general case, panics are designed to lead to program termination, not recovery. Examples like unit tests are a special case - the fact that handling panics work for that case doesn’t mean they would work well more broadly.

The point you mentioned, about being able to configure panics to abort, is another issue. If you did that in a program which used panics as an exception handling mechanism, the program would fail on its first exception. Of course you can say “just don’t do that”, but the point is it highlights the difference in the semantics of panics vs. exceptions.

Also, panics are not typed, the way exceptions are in C++ or Java. Using them as a general exception handling mechanism would either be very limiting, or require the design of a whole infrastructure for that.

The are other issues as well, including behavior related to threads, to FFI, and to where panics can even be caught.

Re: Patterns for Defensive Programming in Rust

#97
post #33

Earlier quoted context omitted.

See https://abseil.io/tips/ for some idea of the kinds of guidance these kinds of teams work to provide, at least at Google. I worked on the “C++ library team” at Google for a number of years. These roles don’t really have standard titles in the industry, as far as I’m aware. At Google we were part of the larger language/library/toolchain infrastructure org. Much of what we did was quasi-political … basically coaxing…

This is a really cool insight, thank you! > Half of the tips above were probably written by interested people from the engineering org at large and we provided the platform and helped them get it published. Are you aware how those engineers established their recommendations? Did they maybe perform case studies? Or was it more just a distillation of lived experience type of deal?

I wasn’t in C++ style land but my recollection is that distilled experience would be backed up by extensive mailing list discussions. in case of contention the discussion might extend into case studies or other quantitative techniques atop google3. It’s difficult for me personally to describe the impact (outsized)of a super-resourced monorepo for this kind of thing. also as gp mentioned, it was sometimes possible to automate changes to comply with updated guidelines.

Re: Patterns for Defensive Programming in Rust

#98

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: sta…

What I do in C is:

    switch (value) {
        case VALUE1: ...
        case VALUE2: ...
        default:
            __builtin_unreachable ();
    }
Where __builtin_unreachable invokes UB.

Re: Patterns for Defensive Programming in Rust

#99

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…

[deleted]
Post reply on HN