Flipping default on must_use/discardableResult

TL;DR: I'm proposing ability to set #[must_use] flag once for all functions in an entire crate/module/impl block.

"must use" behavior in Swift is close to what Rust does, except the default for functions is "must use". Instead of having an opt-in #[must_use], Swift has a @discardableResult annotation that opts out of the default "must use" behavior. I find Swift's default quite sensible and useful.

When I review my code for functions which could have #[must_use], I find that pretty much all of them could. There's a big gray area, but I find that "definitely discardable" results are a minority.

The different default also changes thinking about this feature. Instead of hypothetical "would anyone actually forget to use this value, and would that be a disaster?" to "is ignoring return value of this function part of its intended use?", which is more grounded in API design.

Adding #[must_use] everywhere is a bit of a chore and peppers code with annotations.

I realize Rust can't actually change the default without causing an annoying transition period, therefore I suggest adding an opt-in syntax that flips the #[must_use] default for entire scopes instead.

For example, a crate could have:

#![every_fn_is_must_use]

or module, or impl:

#[every_fn_is_must_use]
impl VeryMuchUse {
   …
}
8 Likes

Would there be a per-function opt-out, similar to Swift's @discardableResult?

Yes, I think that would be useful too. Names and exact syntax need to be bikeshedded.

Or, in Rust's style, maybe this could be generalized one step more.

There's #[cfg_attr(cond, attr)] that sets attributes conditionally. Maybe there could be a similar attribute-setting attribute for automatically adding attributes?

Maybe:

#[default_attr(fn, must_use)]
mod everything_must_use {
    …
}

This could be useful in other cases, e.g.

#[default_attr(fn, inline)]
mod impl_std_ops;

and maybe even:

#[default_attr(struct, derive(Debug)]
mod ffi;
2 Likes

I think the common worry here is warning noise. Like, yes, it's better to not call [T]::len if you don't need to, but did it really matter? The guidance for std seems to roughly be "it's only worth linting if you can point them at something else better instead".

One can always turn on #![warn(unused_results)] (https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html#unused-results) if one wants to hear about everything. (As you say, not using the result is rare, so let _ = in those few places shouldn't be too bad.)

I do think there's a place for a more nuanced lint that people might be more willing to turn on (since I don't see unused_results often). A not-quite-right starting point: tell me about unused results from things things where all the arguments are &impl Freeze but it's not a const fn.

8 Likes

Is the exclusion of const fn because those are already warned for? Or some other reason? (That the compiler theoretically could trivially optimize it out?)

Do note that any non-const fn can call into e.g. log::info! and have side-effects, even if the side-effects are not "purposeful" (i.e. the desired outcome of calling the fn and discarding the result).

Mostly just to try to avoid noise.

It reminds me, for example, of a conversation in a PR I saw the other day about how clippy shouldn't complain about .unwrap_or(Vec::new()) (right now it suggests .unwrap_or_else(Vec::new)) because Vec::new() is const fn and so trivial that it's not worth mentioning the potentially-spurious creation of it any more than it would be to complain about .unwrap_or(1 << 15).

And, as I said, that was a "not-quite-right starting point". If I knew an amazing heuristic for this I'd have opened an issue already :upside_down_face:

Certainly. But that can also be things like calling a cache before you need it as a way of prefetching, where you don't want to remove the call but also don't want to use the result.

To me this isn't a concern. First, Swift has shown that default must_use works fine.

Second, the feature I'm proposing is not really about adding more warnings, but more about being able to have less syntax noise and boilerplate for existing use of #[must_use].

In other words, I want to add #[must_use] to 95% of functions I write. I'm doing that anyway, but current Rust syntax is tedious. My programming style is mainly side-effect-free, which means that majority of the time if I make a method that returns something, the whole point of calling that method is to use that value.

3 Likes

The fact that it’s const fn doesn’t have much to do with that, it’s only because it’s known to be so trivial that makes it ok. calculate_nth_prime(5678) could also be const fn but is a very non-trivial runtime call.

.unwrap_or(const { calculate_nth_prime(5678) }) is going to be interesting when that’s supported.

4 Likes