Live data from Hacker News

Don't unwrap options: There are better ways (2024)

corrode.dev

41–50 of 63 posts

Re: Don't unwrap options: There are better ways (2024)

#41

Talking about unwrapping: I’ve been using a rather aggressive list of clippy lints to prevent myself from getting panics, which are particularly deadly in real-time applications (like video games). unwrap/expect_used already got me 90% of the way out, but looking at the number of as conversions in my codebase, I think I have around 300+ numerical conversions (which can and do fail!) [lints.clippy] all = "deny" unwrap…

This is nice, but fairly miserable to deal with in in-module unit tests, IMO.

We get around it by using conditional compilation and putting the lints in our entrypoints (`main.rs` or `lib.rs`), which is done automatically for any new entrypoint in the codebase via a Make target and some awk magic.

As an example, the following forbids print and dbg statements in release builds (all output should go through logging), allows it with a warning in debug builds, and allows it unconditionally in tests:

    #![cfg_attr(not(debug_assertions), deny(clippy::dbg_macro))]
    #![cfg_attr(not(debug_assertions), deny(clippy::print_stdout))]
    #![cfg_attr(not(debug_assertions), deny(clippy::print_stderr))]
    #![cfg_attr(debug_assertions, warn(clippy::dbg_macro))]
    #![cfg_attr(debug_assertions, warn(clippy::print_stdout))]
    #![cfg_attr(debug_assertions, warn(clippy::print_stderr))]
    #![cfg_attr(test, allow(clippy::dbg_macro))]
    #![cfg_attr(test, allow(clippy::print_stdout))]
    #![cfg_attr(test, allow(clippy::print_stderr))]
AFAIK there isn't currently a way to configure per-profile lints in the top-level Cargo configs. I wish there were.

Re: Don't unwrap options: There are better ways (2024)

#42
I use a setup like this:

https://gist.github.com/tekacs/60b10000d314f9923d6b6a5af8c35...

where... in my code, I have:

  some_block({ ... }).infallible()
for cases where we believe that the Result truly should never fail (for example a transaction block that passes through the inner Result value and there is no Result value in the block) and if it does then we've drastically misunderstood things.

Then, there's an enum (at the bottom of the file) of different reasons that we believe that this should never fail, like:

  // e.g. we're in a service that writes to disk... and we can't write to disk
  some_operation.invariant(Reason::ExternalIssue)
  // we're not broken, the system wasn't set up correctly, e.g. a missing env var
  some_operation.invariant(Reason::DevOps)
  // this lock was poisoned... there's nothing useful that we can do _here_
  some_operation.invariant(Reason::Lock)
  // something in this function already checked this
  some_operation.invariant(Reason::ControlFlow)
  // u64 overflow of something that we increment once a second... which millennium are we in?
  some_operation.invariant(Reason::SuperRare)
... etc. (there are more Reason values in the gist)

This is all made available on both Result and Option.

Re: Don't unwrap options: There are better ways (2024)

#43
post #34

Earlier quoted context omitted.

Yes, exactly. It is the Rust equivalent of goto.

Goto is bad because it results in very difficult to reason about code. Using unwrap and expect is as bad as using any other language without null safety.

goto is bad when it's used in a way that makes it difficult to reason about code, but not all uses of goto are like that. The usual C pattern of `if (err) goto cleanup_resources_and_return_err;` is a good example of the use of goto that is not difficult to reason about.

Using unwrap/expect is still much better than using a language without null safety because unwrap/expect make it immediately obvious at which point a panic can occur, and creates some friction for the dev writing the code that makes them less likely to use it literally everywhere.

Re: Don't unwrap options: There are better ways (2024)

#44
post #16

Earlier quoted context omitted.

hehehe. reminds me of if err != nil in Go which is really not an issue in my opinion. But it seems to have become somewhat infamous in some circles.

The problem is the compiler doesn’t help you if you forget to check err. Although it will flag unused variable. So you will have to make an effort to deliberately ignore the error value. Still not quite as nice as the compiler forcing you to handle the error case.

The problem is that idiomatic Go reuses err for multiple calls. So if you already have one call and check err after, it counts as used, and forgetting to check it on subsequent calls is not flagged.

Re: Don't unwrap options: There are better ways (2024)

#47
post #5

I have been using Zig a lot lately, and I just want to share the equivalent of the let-else solution in Zig: const user = getUser() orelse return error.NoUser; If you only need user for a narrow scope like you would get from match, you can also use if to unwrap the optional. if (getUser()) |user| { // use user } else { return error.NoUser; }

And the same in Python: if user := get_user() is not None: # use user else: # return error Although given the happy path code can mean you don't see the error condition for ages, I much prefer this: if (user := get_user()) is None: # return error # use user

Python doesn't have option, so this is not the same thing at all

Re: Don't unwrap options: There are better ways (2024)

#48
post #35
post #16

Earlier quoted context omitted.

hehehe. reminds me of if err != nil in Go which is really not an issue in my opinion. But it seems to have become somewhat infamous in some circles.

It is a massive problem. It's basically the worse thing about C and they decided to copy it. Easily 60-70% of all go code is about propagating errors to the caller directly.

Not C. C errors are different since they are simply numbers.

And spoiler alert, every language propagates errors. Sometimes automatically via exception handling, somewhat simply by returning error values. rust does this too.

Matter fact, even javascript might be creeping toward this model of explicit error propagation soon.

Good, because it is easier to understand.

Re: Don't unwrap options: There are better ways (2024)

#49
post #16

Earlier quoted context omitted.

hehehe. reminds me of if err != nil in Go which is really not an issue in my opinion. But it seems to have become somewhat infamous in some circles.

The problem is the compiler doesn’t help you if you forget to check err. Although it will flag unused variable. So you will have to make an effort to deliberately ignore the error value. Still not quite as nice as the compiler forcing you to handle the error case.

True. Not a big issue in practice and there are linters and whatnot. Variable shadowing can happen.

But it's a bit orthogonal of a concern. I do have things to say about errors but my complaints are a bit more nuanced and made with hindsight.

Re: Don't unwrap options: There are better ways (2024)

#50

Talking about unwrapping: I’ve been using a rather aggressive list of clippy lints to prevent myself from getting panics, which are particularly deadly in real-time applications (like video games). unwrap/expect_used already got me 90% of the way out, but looking at the number of as conversions in my codebase, I think I have around 300+ numerical conversions (which can and do fail!) [lints.clippy] all = "deny" unwrap…

This is nice, but fairly miserable to deal with in in-module unit tests, IMO. We get around it by using conditional compilation and putting the lints in our entrypoints (`main.rs` or `lib.rs`), which is done automatically for any new entrypoint in the codebase via a Make target and some awk magic. As an example, the following forbids print and dbg statements in release builds (all output should go through logging), a…

We just set all the lints to `warn` by default then `RUSTFLAGS="--deny warnings"` when building for release (or in CI).
Post reply on HN