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.
