`ControlFlow`: The Answer to `BoolAnd`/`BoolOr`

This is an informal proposal (not really Pre-RFC stage, just looking for opinions) of traits for overloading && and || using the power of the ControlFlow type to preserve short circuiting behaviour.

pub trait BoolAnd<R = Self>{
     type Output;
     fn eval(self) -> ControlFlow<Self::Output,Self>;
     fn booland(self, rhs: R) -> Self::Output;
}

(BoolOr would look exactly the same, but boolor instead of booland).

With the above trait, the proposed desugaring for non-primitive x && y is:

match BoolAnd::<typeof(y)>::eval(x){
       Continue(__x) => BoolAnd::booland(__x,y),
       Break(__out) => __out
}

There would be a logical invariant that BoolAnd::booland/BoolOr::boolor agrees with {BoolAnd,BoolOr}::eval - IE. if both values individually return Continue, doing a chained BoolAnd::eval on the Output should yield Continue, and if either x or y breaks with a value after BoolAnd::eval, BoolAnd::eval(BoolAnd::booland(x,y)) should return Break with a logically equivalent value. Further, BoolAnd::eval should break for "falsey" values and continue for "truthy" values, and BoolOr::eval should break for "truthy" values and continue for "falsey" values.

An alternative design used Self for both the RHS and the Output type. I elected to go with this design to show the flexibility of the design but it could be altered to single-type only easily enough. Further, with a combination of assocated_type_defaults, and unstable features, the trait could be extended on nightly only from a single-type design to this one.

2 Likes

What is the motivation for this proposal?

And does the trait necessarily need two methods?

// which types would you implement this trait for?
trait Booland<Rhs = Self> {
    type Output; // what is the point of output? Shouldn't this always return bool?
    // taking in closure would signal that `rhs` might not get evaluated
    fn booland(self, rhs: impl FnOnce() -> Rhs) -> Self::Output;
}

Last I feel the names LogicalAnd & LogicalOr are more descriptive.

I would expect the y in x && y to be able to influence control flow (think for example of .await!) , and FnOnce can't express that.

I'm not sure mine expresses that ether. Though mine would IMO be nicer to lower, and avoids the APITIT.

I think yours would be able to express that because it still evaluates y in the same function/loop scope.

That said, the problem of your desugaring is that typeof(y) is not valid rust, and even though it can probably implemented for special cases in the compiler it would no longer be a desugaring in that case.

typeof is applied for desugarings of other operators, after type inference, is it not? It's really just a placeholder to show which impl of BoolAnd is used.

Ah you meant control flow to external functions.

I believe it's just inference for other operators (though it's true they're not really "desugarings" at the syntax level).

The inference situation is different because other binary operators take both arguments in a single call. Note that any implemented R would compile there -- some sort of typeof is required for said desugaring. Which in turn means the RHS must have an inferrable type on its own, so this wouldn't work with multiple implementations unless one of them was for !,[1] say.

let thing = thing && panic!();

The single-type version doesn't have these issues.


  1. or maybe () today due to the fallback blocking stabilization of ! ↩ī¸Ž

AFAIK other operators are just lovered to OpTrait::op_method(arg1, arg2) and the types here are fully identified by the parameter passed. In your case however in BoolAnd::eval(x) there's nothing that tells the compiler what the R parameter should be.

See for example Rust Playground

I don't think generic parameter R is particularly useful?

Some prior conversations about using ControlFlow for this: https://github.com/rust-lang/rfcs/pull/2722#issuecomment-832198425

1 Like

Better still might be ShortCircuitAnd and ShortCircuitOr; without knowing about the intended use cases for overloading this, I'm not sure whether attaching "Bool" or "Logical" to it makes sense.

7 Likes

Scand and Scor perhaps?

Why shorten them, especially by so much? I don't expect them to show up often (if at all?) in trait bounds, and when/if they do show up, people can do that rename locally.

6 Likes

It would be great to have a solution for this -- I've definitely thought about DSLs that could benefit from being able to override short-circuiting And and Or operations.

3 Likes

Previous discussion: Pre-RFC: Overload Short Curcuits