No discarding return values by default?


#1

This split off from https://github.com/rust-lang/rfcs/pull/886#issuecomment-279126185 .

The gist is that, instead of marking types or specific functions with #[must_use], maybe we should do the opposite and mark specific functions with #[can_ignore], such that no return values can be ignored by default with “;”.

Many people mentioned that this was considered in the past but was decided against because of methods like Vec::push possibly returning the item pushed. I’m interested to know how pervasive this kind of functions are, because it seems to me that they should be in the minority, and we could simply annotate them with #[can_ignore] to say that “yes, it’s safe to ignore the return values of this method”.

I realize that this is a half-sailed ship, and introducing this as even a warning could be considered too backwards-incompatible. But still it might be interesting to see whether it’s a feasible idea in a vacuum, and even whether it can possibly be introduced as a very soft warning first.


#2

I’m pretty sure this can’t happen in rust without breaking everything but would definitely make a nice clippy lint (although I’m not sure if clippy is powerful enough to do this).

  • Due to rust’s move semantics, lots of removal methods return the element(s) removed.
  • Some methods return “handles” that you may not need to keep (e.g., thread::spawn).
  • Throwing away the result of Iterator::count is a common “loop through this iterator” idiom (although I’m not really a fan of doing that).
  • Read::read_to_end(...).unwrap() (and friends) returns the number of bytes read which you usually don’t care about.

On the other hand it would help catch my_file.write(...).unwrap() (should be my_file.write_all(...).unwrap()).


#3

There is an existing lint to pick this up (unused_results), although it doesn’t support any kind of annotation at function definition to disable it.


#4

To me, the first two points sound like ideal targets to mark as #[can_ignore]. The Iterator::count one can easily be worked around by doing “let _ = iter.count();”, which also helps to signal that something funky is going on.

The last one is a bit tricky, as it sounds like we should only be marking the Ok(...) part of the return value of Read::read_to_end as #[can_ignore].


#5

I rarely use #[must_use]. Result covers common cases. The only common case I miss is for pub fn new() -> Self.

But other than that it’s easier for me to find counter-examples, e.g. builder pattern would be affected. Builder::new().foo().bar() vs let mut b = Builder::new(); b.foo(); b.bar(). I sometimes need the latter when setting options from loops.

I could use #[can_ignore] already. I have setters that can fail if you pass bogus parameters, so I want to reflect that in the API and I want to use Result for well-known type and compatibility with the try? syntax. However, in normal programs the failure is very unlikely, so I don’t want to force users to always check the result.


#6

The last one is a bit tricky, as it sounds like we should only be marking the Ok(…) part of the return value of Read::read_to_end as #[can_ignore].

To do that, you’d need some way to propagate can_ignore (e.g., through some kind of “taint” system). When doing the analysis, you’d treat the return type as Result<usize @can_ignore, io::Error> where T @can_ignore can be coerced into T when necessary (e.g. if cond { 1 : usize @can_ignore } else { 2 : usize } : usize).

That would get very complicated.