The ? operator will be harmful to Rust

I'm sorry but that makes no sense even as a contrived example. An invalid value would not propagate out of the function in the Ok part of the value. If you have that situation that the same object might be both valid and invalid depending on how you look at it is a dangerous thing to do and ? is the least of your worries in that case.

1 Like

That has a lot of downsides:

  • it keeps you in the function instead of early returning which means you keep executing code even though you already gave up. Same issue you have with lots of map calls currently.
  • It does not come up as an actual problem in Rust code today. You can already use map and people do not do this for error handling.
  • It does not support From/Into for error result conversion because the target type is not known.
  • Very confusing to read because nobody knows what's happening any more. I experimented with this plenty and code becomes utterly unreadable because of where the actual returns are then happening. You can try this yourself by taking some code that currently uses ? for early returns and try to convert it into your proposed behavior (sans the type conversion, so that makes it super bulky but ignore that for the moment). It becomes unreadable and complex and in no way more convenient than to just use map.
1 Like

You can still intersperse check!() calls if that is a problem. Result-chaining has also advantages, it allows error handling other than early returning the error value, like:

match foo()?.bar()?.filter() {
   Ok(value) => ...
   Err(e) => return &[],
}

Map is clunky and requires an explicit closure.

This is actually the only point that I agree with and that I wasn't aware of.

I'd say it's the opposite. The check!() makes it crystal clear where the returns are happening. With ? for early return, returns can happen basically everywhere. I'd like to have as few "points of return" as possible, as many as necessary.

I guess all we can do is acknowledge that we disagree...

1 Like

I don't believe that you make a new type every time this can happen.

Here's a very contrived example:

// Error handling in caller
fn find_something(...) -> usize {
    ...
}

fn use_find() -> Result<...> {
    let x = find_something(...);
    if (x < 5) {
        return Err(...);
    }
    // ...
}

// Error handling in callee
fn find_something_2(...) -> Result<usize, ..> {
    ...
    if (x < 5) {
        return Err(...);
    }
}

fn use_find_2(...) -> Result<...> {
    let x = find_something_else(...)?;
    // Missing error handling here?
}

I don't make this up. It really happened to me more than once. It was was actually code from libstd that already uses ? pervasively. I don't remember the exact location though.

It is a problem. It frustrates me that discussions are being held here about topics that have been covered for a long time. I spend an awful amount of time trying different types of error handling over the last year. I can tell you from personal experience of many, many hours of trying various different things that ? as it's being proposed is by far the best proposal to a practical problem and none of the proposed alternatives either a) have been tried properly b) cover all the cases or c) can be implemented in current rust.

I know it's easy to bikeshed about this and have opinions but I really encourage people that are interested in this to actually get their hands dirty and take a large Rust application and implement various different error handling systems for themselves. You might be surprised what feels natural and what does not.

The clunky part is not the closure, the clunky part is fighting with type inference.

Nobody cares where the return is happening. It's basically not an issue that is worth discussing. What valuable information do you get out of the returns? We might as well have a discussion about where the panics are happening.

3 Likes

And this is why this discussion is pointless: instead of making contrived examples look at actual code out there and experiment with error handling in that context.

4 Likes

? is implemented and works. Other people have rewritten their projects with ?. The majority are happy about the outcome.

If you want your arguments to have significant weight in this part of the RFC process you really need to show an example that uses ? and compiles. You can then praise or criticize the use of ? in that example.

Even better would be if instead of coming up with an example about a contrived case, you take as an example a piece of error handling code from a real rust project that is widely in use.

That would make your arguments have much more weight.

2 Likes

I only make contrived examples because I don't remember the real code and I don't have the motivation for spending an hour searching old code. The discussion is not pointless because of this. I gave you at least an example, you just state that it will be an unreadable mess,.

Maybe the ? for result chaining is not a good choice, I don't know because it's not implemented. But what I can say is, that I prefer the current, (relatively clunky) try! syntax to the ? operator. And I don't think that try! is optimal, we can do better. I won't use ? in my code.

I do care. Panics are a different beast. Those are logic errors that should not happen.

And BTW it was an answer to this:

So, do you care or not?

1 Like

Why?

But not because of where the returns are happening but because the code that makes those returns is utterly unreadable. You can try it for your self to see what I mean. It turns concise code into ugly looking constructions to make the type system happy.

Which of the 3 statements do you mean?

I agree that in current rust this is probably the case. But it doesn't have to be the same with the proposed feature. Similar how the current ? is not just a simple return, a chaining ? doesn't have to be equivalent to a simple map. I believe most of the logic to satisfy the type system can actually be part of the ? operator. Or part of the check! macro. But it's really difficult to say without an actual implementation.

Why you care.

That's why I experiment with this on an actual implementation for a few weeks now :slight_smile:

The only issue I worry about this operator is that some people donā€™t like sigil and this proposal obviously make them more unhappy.

I have to admit this syntax is more or less perl-style, but Iā€™m fine with that. Rather, I hate semantics around spaces much more.

Note the try!() syntax use three sigils instead of one.

3 Likes

What if the ?. operator just maps the result to another result instead of resolving it? That way the returns would be more explicit again: try!(foo()?.bar()?.baz())

1 Like

Well try!() is ident along with sigils, and bare sigil make it harder to guess the meaning when you first see it, sometimes even harder to remember when you see it the second time. But you made a good point :+1:

Except that:

  1. Those sigils are all general-purpose things used throughout the language. Parens are ubiquitous (but kinda overloaded; I canā€™t say Iā€™m happy with parens for function calls, precedence overrides, and tuples). The exclamation point is used for all the macros (as well as NOT, but thatā€™s different enough to not require special disambiguation syntax hacks).

  2. I like having some sigils. The alternative is lots of keywords.

Itā€™s worth noting that the ? feature is up for stabilization this release cycle: https://github.com/rust-lang/rust/issues/31436

Iā€™ve added a link to this thread to that issue.

1 Like

Is somebody proposing to deprecate try!? Because if not I donā€™t see why you shouldnā€™t use what you think is better/more readable for your projects.

I donā€™t know if itā€™s been mentioned before but would using the ? for error handling prevent it from being used as a shortcut for nullable types? Same sigil for different use would be confusing.

I.e. Option<Path> -> Path?

Has that syntax been mentioned before?

There's talk about a Carrier trait for ? to support Option, but that would be used the same way, basically "unwrap this value or return None".

What you describe looks like a shorthand in the type system, where the current syntax is used in expressions, so at least they're different domains. It might be confusing compared to the current ?Sized though, which has nothing to do with Option.