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.
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 usemap
.
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...
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.
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.
?
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.
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?
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
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.
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())
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
Except that:
-
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).
-
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.
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
.