? 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.

14 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.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.