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 beforeor
and replace it with the value on RHS ofor
because information that must be handled explicitly then could be silently lost. -
Option<T>
is nullable: itsNone
variant can be safely replaced with the value on RHS ofor
because there’s nothing to lose; and itsSome(T)
variant can be safely converted into the type of value onor
RHS 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
or
operator is overloadable bystd::ops::Or<T>
trait, which allows for implementing type to appear on the LHS ofor
operator
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 EXPR
intomatch
expression 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 EXPR
intoif
expression 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
and
operator somewhere - Uses non-reserved keyword
Alternatives:
- Don’t implement it
- Use another symbol instead of
or
e.g.:?
,else
,<>
- Extend
||
operator to be used in the same way asor