Allow `?` in unit-returning methods

To avoid forcing the API to use Option<()> as the return type when there is none, ? operator could be allowed to have cleaner early returns.

By the way, Option<Infallible> can be used too.

2 Likes

If we get Add `homogeneous_try_blocks` RFC by scottmcm · Pull Request #3721 · rust-lang/rfcs · GitHub then this can be done just by wrapping the body in try { … };.

2 Likes

this can be done just by wrapping the body in try { … }; .

Good to know, thanks. But I can't think of any reason not to allow it everywhere. The difference in codegen would be return as opposed to return v for Result and Option.

1 Like

I'm note sure whether this is still close enough to the topic, but I recently had a method returning Result<ExitCode, ExitCode> to allow early-returns using the ? operator. The function guaranteed that T was always ExitCode::SUCCESS and E was always ExitCode::FAILURE. What I was missing there was something like unwrap_either on Result<T, T> like:

impl<T> Result<T, T> {
    unwrap_either(self) -> T {
        match self {
            Ok(value) => value,
            Err(value) => value,
        }
    }
}

The Try trait can be used to support ? for ExitCode, and even bool.

This was proposed, but rejected for stabilization; see Tracking Issue for `Result::into_ok_or_err` / `feature(result_into_ok_or_err)` · Issue #82223 · rust-lang/rust · GitHub for the details. However, note that even without the method, it can be expressed in a more concise form than a full match:

let (Ok(exit_code) | Err(exit_code)) = result;
5 Likes

I feel like it would go better with the grain of the existing language if you instead used Result<(), ()> for situations where you only care about whether an error happened, not any specifics. You would convert that to ExitCode at the point where you actually need an ExitCode.

Hmm... how do we feel about impl From<Result<(), ()>> for ExitCode, folks? Maybe also From<Result<(), NonZeroU8>>.

3 Likes

This sounds like you want to be able to use ? on Option<T> in -> () functions to early-exit on None? While perhaps the type shapes line up, this is a bit of a weird use case. We deliberately don't allow ? to interconvert between Option<T> and Result<T, ()>, and this feels like a similar "allowable but questionable" interconversion.

If the function always "returns None," then -> Option<!> (or Option<Infallible> on stable) is a reasonable choice. If the function sometimes succeeds, then -> Option<()> carries meaningful information (whether it succeeded or bailed early). If there's only really one exit condition (e.g. "work done"), then -> () is most appropriate, and let Some(val) = val else { return }; is reasonably clear.

Plus, a try block in the body should be the eventual "correct" way to model aggregating multiple ?-using ops inside a non-?-compatible return type context.

I agree that Result<(), ()> is the better way to model such. Termination::report is almost the conversion to ErrorCode that you want as well, except that it also tries to dump the impl Debug of the error to stderr as a side effect.

There's also the fact that ExitCode tries very hard to be resilient to any possible future exotic target. One could exist where there are multiple allowable success codes. ExitCode is intended to be a non introspectable black box to send to process termination, AIUI, so implementing Try would break that.

The immediate feeling is that it's redundant with Termination, but the stderr side effect means it's useful in different situations[1].

I kinda want to extend it to any of Infallible, !, () or ExitCode in the Ok slot, even. (To match the std set of Termination types, sans nesting Results or non-unit Errs.)

We don't actually document a guarantee that SUCCESSrs is zero. C defines that both 0 and EXIT_SUCCESSc indicate successful termination, and does not require EXIT_SUCCESSc to be 0.

We document that a termination of () (producing ExitCode::SUCCESS) exits the process with libc::EXIT_SUCCESS, and thus we allow for it to be a nonzero value. So this doesn't quite model all situations that ExitCode is trying to support.

There's still significant merit, don't get me wrong, but it isn't as simple as mapping unit Ok to SUCCESS and unit Err to FAILURE.


  1. Essentially, even though it seems related, Termination is just for customizing return-from-main behavior. ↩︎

2 Likes