Might ??
be a beter alternative (like C#)? So instead of:
let x = something_that_is_fallible() ?: return;
you could have:
let x = something_that_is_fallible() ?? return;
or even any of:
let x = something_that_is_fallible()??return;
let x = something_that_is_fallible()? ?return;
let x = something_that_is_fallible() ? ? return;
without ambiguity. Although, I'd personally prefer that is only parses if it is connected like ??
and not ? ?
Woudn't ??
fit better with current Rust grammar and parsing rules as well as with current idiomatic code?
So, we'd have ?
operator for "early return on failure of fallible things" where the error is propagated/returned and ??
for "map failure to whatever you choose including an expression that evaluates to () and returns or breaks".
It seems like ??
would fit better both cognitively and be a better fit for existing grammar. Also, it aligns pretty closely with ??
in C# which is a "Prior-Art" bonus.
EDIT: It would also be nice to support something like:
fn a() -> Result<Actual,Error> {}
fn b(self : Actual) -> Option<Value> {}
fn c(self : Actual) -> Option<Value> {}
fn d() -> Value {}
let x = a() ? b() ??? c();
let y = a() ? b() ?? d();
where the above is equivalent to:
let x : Actual = match a() {
Some(a) => a
Error(b) => return Error(b);
}
let x : Option<Value> = match x.b() {
Some(a) => Some(a)
None => x.c()
}
and
let y : Actual = match a() {
Some(a) => a
Error(b) => return Error(b);
}
let y : Value = match x.b() {
Some(a) => a
None => d()
}
EDIT 2: It might even make sense to support the following:
fn a() -> Result<Actual,Error> {}
fn b(self : Actual) -> Option<Value> {}
fn c(self : Actual) -> Option<Value> {}
fn d() -> Value {}
fn e(self : Actual) -> Option<Value> {}
fn f(self : Actual) -> Option<Value> {}
let x = a() ? b() ??? c() ???? e() ?? d();
which would be preferably formatted like:
let x = a()
? b()
??? c()
???? e()
?? d();
and would be equivalent to:
let x1 : Actual = match a() {
Some(a) => a
Error(b) => return Error(b);
}
let x2 : Option<Value> = match x1.b() {
Some(a) => Some(a)
None => x.c()
}
let x3 : Option<Value> = match x2 {
Some(a) => Some(a)
None => x2.e()
}
let x : Value = match x3 {
Some(a) => a
None => d()
}
EDIT 3: This could continue to be extended like:
let x = a()
? b()
??? c()
???? e()
????? f()
?? d();
Which would be saying:
Let x be equal to the unwrapped result of a()
(or early return the error) nvoking a().b()
unless a().b()
returns Result or Option, in which case use the value a().c()
unless it returns a Result or Option, in which case use the value a().e()
unless it returns a Result or Option, in which case use the value a().f()
unless it returns a Result or Option in which case use the value d()
.
This idea can be extended to any number of consecutive ?
operators to allow for easy traversal of trees of optional/fallible functions/methods to obtain the first non-failure/None value at multiple depths and branches in a fairly intuitive manner. You can liken the ?
to .
in file-system paths and ??
to ..
and ???
to ..\..
etc. (NOTE: It's not an exact analogy, but, I think the intuition carries over reasonably well).
You could also do (changing d() to be fallible as follows):
fn d() -> Option<Value> {}
let x = a()
? b()
??? c()
???? e()
????? f()
?? d()
?? return Result(Error);
EDIT 4: Upon further reflection, I think it might actually be better to represent as follows:
let x = a()
? b()
??? c()
??? e()
??? f()
?? d()
?? return Result(Error);
That way this could make sense and be useful:
let x = a()
? b()
??? c()
?? c1()
??? c2()
??? e()
??? f()
?? f1()
??? f2()
??? f2_1()
?? d()
?? return Result(Error);
The above would evaluate as:
If a() returns Error or None - early return with error propagation; otherwise, the value returned is a().b() unless a().b() returns a failure value, then if a().c() is failure then the value is c1() unless that is a failure, then the value is a().c().c2() unless that returns failure, then value is a().e() unless it returns failure then if a().f() returns failure then the value is a f1() unless that returns failure, then the value is a().f2() unless that returns failure, then the value is a a().f2_1() unless that is a failure, then the value is d() unless that is a failure, then a return is performed with an error.
Basically the ?n
operator can be interpreted as follows:
?
- return/propogate error from lhs or return evaluate to value of rhs using the lhs as the receiver (self)
??
- evaluate to value of lhs unless it is Error/None, then evaluate to value of rhs
???
- evaluate to value of lhs unless is is Error/None, then evalute to value of rhs using the previous non-error value in the chain as the receiver (self)
????
- evaluate to value of lhs unless is is Error/None, then evalute to value of rhs using the 2ND-previous non-error value in the chain as the receiver (self)
?????
- evaluate to value of lhs unless is is Error/None, then evalute to value of rhs using the 3RD-previous non-error value in the chain as the receiver (self)
- ...
??{4,n}
- evaluate to value of lhs unless is is Error/None, then evalute to value of rhs using the Nth-previous non-error value in the chain as the receiver (self)