This proposes a new operator which is similar to ?? from Swift and ?: from Kotlin.
That could be a useful addition to Rust because match/ if let constructs sometimes are an overkill when dealing with nullable values, and single alternative to them - a diversiform combinators on Option<T> is hard to use especially for newcomers, since there’s no simple rule how to remember them and how to choose a proper function, plus Result<T, E> makes that even harder by implementing the same functions with different signatures, and after all some functions have the same downsides as closures: no ? operator inside, no early return/break, no possibility to mutate bindings in parent scope, unnecessary borrows, verbosity.
// An example of intended syntax
let num: i32 = Some(0) or Some(1) or 2;
// An example of the same in current Rust (for comparsion)
let num: i32 = Some(0).or_else(|| Some(1)).unwrap_or_else(|| 2);
Why exactly have this syntax:
- It perfectly describes action and it’s probably impossible to find something more expressive
- It’s compact, which is good for consistency and for alignment of code
- It wouldn’t be misinterpreted as a new boolean operator, which is good for understanding of code
// E.g. some symbolic sigil could be confusing here
return Some(0) ?: 1;
As a possible extension it also might be implemented to act as a ternary operator. Even if there’s many reasons to not introduce a ternary, this proposal bypasses some of them because resulted syntax and semantics would be very different.
// An example of ternary `or`
let num = condition() && 0 or 1;
This might be better than a usual ternary because && operator and its lazy nature here are reused, and or operator behaves just like || only indicating a threshold between <bool> and <T>. We only need to handle EXPR or EXPR after && differently than all other expressions.
The main idea behind that is to represent <bool> && <T> as Option<T>, e.g. on a && b && c or d expression a && b && c is the same as if a && b { Some(c) } else { None }.
This syntax will be possible only before or operator:
// The following line still wouldn't compile!
let _ = condition() && 0;
Either, in optional or in ternary context we would choose between a real values, which means that some conversion from/into wrapper types can be performed automatically by Rust awesome type inference. More specifically, a last value in or chain would determine return type of a whole expression.
And only nullable types would be permitted on a LHS of or operator to not make this too implicit, e.g.
-
Result<R, E>is not nullable: it cannot drop itsErr(e)variant beforeorand replace it with the value on RHS oforbecause information that must be handled explicitly then could be silently lost. -
Option<T>is nullable: itsNonevariant can be safely replaced with the value on RHS oforbecause there’s nothing to lose; and itsSome(T)variant can be safely converted into the type of value onorRHS because wrapper no longer needed and the target type is obvious.
let res: Result<i32, &str> = Some(0) or Err("missing zero");
Under the hood it seems to be relatively simple:
- The
oroperator is overloadable bystd::ops::Or<T>trait, which allows for implementing type to appear on the LHS oforoperator
trait Or<T> {
fn into_option(self) -> Option<T>;
}
-
Nullable types implements it, e.g. the following is a default implementation for
Option<T>
impl <T> Or<T> for Option<T> {
fn into_option(self) -> Option<T> {
self
}
}
-
std::convert::From<T>is used to “optimistically” construct a RHS type from LHS value, e.g. the following is a default implementation forResult<T, E>
impl <T, E> From<T> for Result<T, E> {
fn from(t: T) -> Self {
Ok(t)
}
}
- Desugaring converts sole
EXPR or EXPRintomatchexpression with the use of traits from the above
let x = first or last;
/*
let x = match Or::into_option(first) {
Some(o) => From::from(o),
None => last
};
*/
let y = first or next or last;
/*
let y = match Or::into_option(first) {
Some(o) => From::from(o),
None => match Or::into_option(next) {
Some(o) => From::from(o),
None => last
}
};
*/
- Ternary desugaring converts a whole
... && EXPR or EXPRintoifexpression that also uses the same traits inside
let x = a && b && next or last;
/*
let x = if a && b {
From::from(next)
} else {
last
};
*/
let y = a && first or b && next or last;
/*
let y = if a {
From::from(first)
} else {
if b {
From::from(next)
} else {
last
}
};
*/
A problem here is that the following desugaring fails to compile:
match Some(0).into_option() {
Some(o) => From::from(o),
None => return,
};
/*
error[E0277]: the trait bound `(): From<i32>` is not satisfied
--> src/main.rs:190:20
|
190 | Some(o) => From::from(o),
| ^^^^^^^^^^^^^^^ the trait `From<i32>` is not implemented for `()`
*/
This because type inference tries to do something like ()::from(o), which obviously fails.
Implementing From<T> for () might lead into surprising results e.g. (Some(x) or return) would return () which would make impossible calling a function that’s expected to belong to x.
Overall, this is not a big deal, since code that leads to such desugaring wouldn’t be popular, and when it occur we can add type hint to make it compile.
However, having a solution would be better.
Some additional examples:
/// Current Rust
set_visible(view, if condition() { Visible } else { Gone });
set_visible(view,
if condition() {
Visible
} else {
Gone
});
set_visible(view, if condition() {
Visible
} else {
Gone
});
/// Proposal
set_visible(view, condition() && Visible or Gone);
set_visible(view,
condition() && Visible
or Gone);
set_visible(view, condition() && Visible
or Gone);
/// Current Rust
let x = if y { 0 } else { 1 };
let an_option = if condition() {
Some(try_get_value()?)
} else {
None
};
let val = opt.ok_or_else(|| My::Error)?;
/// Proposal
let x = y && 0 or 1;
let an_option = condition() && try_get_value()? or None;
let val = (opt or Err(My::Error))?;
/// Current Rust
let name = condition().as_option().map(Something::create)
.or_else(|| call(10).unwrap().unwrap())
.or_else(try_alternative)
.or(next_option)
.or_else(|| inner_closure(|x| x + x))
.or_else(|| a_result::<Generic>().ok())
.or_else(|| (condition() || other_condition::<i32>()).as_opt().map(Foo::new));
.unwrap_or_else(local_value)
.do_something()
.ok_or_else(|| if foobar || baz { asdfasdfa() } else { asdf() })?;
/// Proposal
let name = (
condition() && Something::create()
or call(10).unwrap().unwrap()
or try_alternative()
or next_option
or inner_closure(|x| x + x)
or a_result::<Generic>().ok()
or (condition() || other_condition::<i32>()) && Foo::new()
or local_value
)
.do_something()
or (foobar || baz) && asdfasdfa()?
or asdf()?;
Drawbacks:
- This might complicate language a bit
- There also would be yet another way to do the same thing
- Might confuse Python users
- Gives impression that there’s
andoperator somewhere - Uses non-reserved keyword
Alternatives:
- Don’t implement it
- Use another symbol instead of
ore.g.:?,else,<> - Extend
||operator to be used in the same way asor