Sketch of overloaded short-circuiting operators
— Rust
Rust’s std::ops
module provides a variety of traits for overloading Rust’s operators, but conspicuously skips the &&
and ||
operators. While I don’t believe these operators are that important to overload, I wanted to attempt to address one of the barriers to that functionality, namely short-circuiting, and provide a starting point for someone who actually cares to write an RFC.
As a refresher, short-circuiting is the behavior that these operators sometimes don’t evaluate their right-hand sides at all, depending on the value of their left-hand sides (true || x
is always true
, false && x
is always false
). This prevents representing these operators with function calls, because a functions’s arguments must be evaluated before it can be called. Even a closure isn’t enough - because an expression like lhs && return
is valid, but cannot be transformed to and(lhs, || return)
, which has different semantics.
A more involved approach is needed. Fundamentally, these short-circuiting operators evaluate the left-hand side and depending on that value either short-circuit to a result or evaluate the right-hand side and combine it with the left-hand side in an operation. To represent these two steps, our trait will need two methods, one to represent the short-circuit check and one for the actual operation. For the rest of this post, I will be speaking about the &&
operator; the ||
operator works pretty much the same.
trait And<Rhs=Self> {
type Output;
fn and_short(&self) -> Option<Self::Output>;
// alternate:
fn and_short(self) -> Result<Self::Output, Self>;
fn and(self, rhs: Rhs) -> Self::Output;
}
In this sketch, and_short
takes &self
because if it returns None, we need to pass that same self
to and
. If we wanted to allow moving in and_short
, its signature could instead be changed to the listed alternate, where Ok(v)
is a short-circuited result and Err(s)
returns the value to be used as the self value of and
.
The desugaring of lhs && rhs
then becomes fairly straightforward. To eliminate ambiguity, I’ve represented it as a macro:
macro_rules! and {
($lhs:expr, $rhs:expr) => {{
let lhs = $lhs;
match And::and_short(&lhs) {
Some(value) => value,
None => And::and(lhs, $rhs),
}
}}
}
The left-hand operand is always evaluated immediately, then and_short
is called. If it returns Some
, the right-hand operand is not evaluted; if it returns None
, the right-hand side is evaluated and and
is called. Here’s how an implementation for bool
might look, if we needed to implement it in Rust:
impl And for bool {
type Output = bool;
fn and_short(&self) -> Option<bool> {
match *self {
false => Some(false),
true => None,
}
}
fn and(self, rhs: bool) -> bool {
match (self, rhs) {
(true, true) => true,
_ => false,
}
}
}
And here’s what an implementation for a hypothetical ternary logic value might look like:
enum Tri {
False = -1,
None = 0,
True = 1,
}
impl And for Tri {
type Output = Tri;
fn and_short(&self) -> Option<Tri> {
match *self {
Tri::False => Some(Tri::False),
_ => None,
}
}
fn and(self, rhs: Tri) -> Tri {
match (self, rhs) {
(Tri::False, _) |
(_, Tri::False) => Tri::False,
(Tri::None, _) |
(_, Tri::None) => Tri::None,
(Tri::True, Tri::True) => Tri::True,
}
}
}
Note that it’s important for consistency of behavior that for values of self
where and_short
returns Some(v)
, and
returns v
for any value of rhs
.
A full example, including an Or
trait is available as a gist and can be run on the playground. Thanks to those on the #rust
IRC who inspired me to want to write this post and poked holes in my earlier ideas.