? in functions that do not return Result<T>

Curiously, what are the arguments you would have for or against allowing ? in functions that do not return Result<T>.

The behavior in question would be to essentially panic with the error, treating all ?'s as an unwrap. A slightly modified panic message could suggest returning a Result if this panic gets triggered in debug mode. It could also be a warning by clippy, but I find myself wondering why we can't do this. It would not affect backwards compatibility.

Obviously opinions are a reason this probably won't be a reality, but "What are those opinions?" - I ask myself

Summary:

  • Allow ? in functions that don't already return Result<T>, treating ? as an unwrap
  • Have a modified panic message in debug mode, suggesting the developer return Result<T>
  • Have a default clippy warning which suggests returning Result<T>

Pros:

  • Better rapid prototyping support in Rust
  • The change wouldn't break prior code. Allowing something previously not allowed.

.unwrap() is generally discouraged, in favor of .expect() which can provide more context.

Using ? as first-class syntax for unwrap would both encourage it and do it in a way that could superficially be confused for proper Result-based error handling.

13 Likes

? is already allowed in functions that return Option<T>, ControlFlow<T, U>, and some Poll<_>s.

This would result in a drastic change in behavior when implementing Try (once stable) for an already-existing type. I.e. it arguably affects forwards compatibility / what is a breaking change.

One could write a panicking implementation to get the behavior you describe (once stable), but I imagine it would be frowned upon generally.

6 Likes

I don't see this ever happening without some sort of opt-in marker.

Generally my answer for "I want ? to do something different inside my method" is that try{} blocks will handle it.

For example (using a not-in-nightly syntax, see zulip) one could imagine something like

fn foo() -> T {
    try as QuestionMarkMeansUnwrap<_> {
        stuff()?;
        goes()?;
        here()?
    }.0
}

That works under try_trait_v2 by having an implementation like

pub struct QuestionMarkMeansUnwrap<T>(pub T);
impl<T, R: Debug> FromResidual<R> for QuestionMarkMeansUnwrap<T> {
    fn from_residual(r: R) -> Self {
        panic!("{:?}", r)
    }
}
8 Likes

How about implementing Try trait for unit type () so that we can early return with ?.

? only makes sense if you have two variants, one for early return and one to continue with. () only has one variant.

2 Likes

I'm generally satisfied with what I've seen in the Try trait; One can just wrap everything they need in a try block, although there is a papercut to this we've already experienced with unsafe.

For example, in:

unsafe {
    let x = 4;
    let y = *(0 as *mut u8);
}

The problem is that unsafe allows you to put safe code inside an unsafe block, making it ambiguous as to what the unsafe code within is (without a closer look).

That's a bit of an ergonomic papercut, and it's not a great idea if we start seeing the pattern:

fn foo() {
   try {
        ...
        ...
        ...
        ...
        ...
   }.unwrap()
)

In the case of most rapid-prototyping scenarios.

So, I'm not sure if I'd rather see the encouragement of ? used individually where needed, as shorthand for unwrap(), but if not, we will see the even messier solution above evolve.