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.