Pre-RFC: Overload Short Curcuits

I was reading through the documentation on over-loadable operators and it said that the two short circuit operators are not possible to be described in traits. But I think that it should be possible. Here is my proposal and the combined semantics.

pub trait BoolOr<Rhs=Self> where Self:BitOr<Rhs, Output=O> {
    /// The resulting type after applying the `||` operator.
    type Output = O;

    /// Performs the `||` check.
    #[must_use]
    fn is_truthy(self) -> bool
}

With the following semantics

A || B

is equivalent to

if A.is_truthy() { A } else { A | B }
4 Likes

Related (cc @scottmcm) :

1 Like

Yes it does seem related. But I really dislike the or_else and and_than methods and such because they use lambdas which don’t play nicely at all with early returns. I feel that having to use them at all is indicative of some unfortunate downfalls in the language. I also feel that the chain

let config = get_first("name").or_else(|| get_second("other_name")).unwrap_or("default");

Is an unfortunate state where it is hard to tell if using or_else with a closure is better (more performant, or something else) than using or and precomputing the value.

Where as the best is probably to use if statements, but that is also clunky. I would say it is much nicer to do something like:

let config = get_first("name") || get_second("other_name") || "default";
3 Likes

This seems no different from C++'s operator bool(), which I strongly believe was a mistake; every reasonable conversion to bool is going to be so short so as to be effortless (and, if it isn’t, you should not be using operator bool()). This is to the point that I feel the need to ding people for writing if (ptr) instead of if (ptr != nullptr) in C++ review, because people are entirely too clever about implicit conversions.

5 Likes

This is different because I am not advocating for coercion to boolean and this syntax isn’t that at all. This is much more like the null coalescing operator from C#. And it is not like we cannot do this already, it is just either awkward (using macros) or probably less performant (using or_else and closures)

3 Likes

Here are examples of what I expect if Option and Result implemented this operator:

let n=Some(100);

// This doesn't panic.
assert_eq!(n || panic!(),Some(100));
let ok=Ok(100);

assert_eq!(
    // This doesn't allocate 1GB of memory
    ok || Err(vec![1u8;1_000_000_000]),
    Ok(100)
);
// Compiler error:expected bool,found Option<i32>
if Some(100) {}
if let Some(20)=(Some(10) && Some(20)) {
     // I expect this to run
     println!("Inside!");
}
if let Err("oh no!")=(Ok(()) && Err("oh no!")) {
     // I expect this to run
     println!("Inside!");
}
8 Likes

I agree with everything here.

operator bool's problems are exactly the same as the problems described here: types can be truthy and can participate in non-trivial conditionals, which I think makes code more sigiltastic and far less readable. I have never been a fan of ?: and friends.

I'm pretty sure that or_else and friends are aggressively inlined, so performance is not a concern. If they aren't aggressively inlined, I think we have some low-hanging fruit in codegenland. There's really no reason to be using macros for this stuff.

4 Likes

I do not see the argument at all for less readable code. In my example above both are explicit on what they are doing, sure it is less noisy but it is a lot more enjoyable and quick to write than the first one.

Also, again it is not the same because individual values don’t get coerced to boolean and second the values returned are generally not boolean anyway

1 Like

return a || b

return a.or_else_with(|| b)

If I am not familiar with your code, one of these is way more obvious than the other, and makes the control flow extremely clear. Given that other languages (like C++) do NOT enable custom short-circuiting || (for example operator|| in C++ is eager!), this is definitely a potential source of confusion. Moreover, having more than one way to express the exact same thing, where one is not clearly better, is definitely bad for readability, because I have to remember that both of these mean the exact same thing (or, we'd need to deprecate or_else_with, like we did with try!).

That something is quick to write is not a good justification for syntax. Code is read orders of magnitude more often than it is written, and saving on typing (which your editor can totally tab-complete) has a lot less value than making code easier to read.

That's not relevant to my point. My point is that non-boolean values should not participate in short-circuiting control flow, which is already a source of subtle bugs.

7 Likes

But they already do with or_else, it is just don't in a function that you are not currently writing.

This is a valid point and code being readable is extremely important. However, we also have to consider how much the reader has to process to understand what the code is trying to do. I find that using || to set default values very easy to understand (maybe it is because I have done a lot of work in JS and C#).

1 Like

The first one?

The second one could store the result of calling the closure before it evaluates whether to return either a or b.

It's fine to deprecate methods that don't exist :wink: .

Things which might make overloadable short circuiting redundant

try{} blocks,which would cover many cases where short circuiting is needed.

if-let-chains,which might conflict with overloading the short circuiting syntax.It's the reason I parenthesized the expression in the if let examples.

1 Like

That is not the same. To call or_else, you need to utter closure syntax, which has a very clear intention attached to it: "I expect this execution to be the same". This is not the case for bare expressions in any other non-macro context.

To process what an overloaded operator does, I need to associate a "non-standard" behavior to that operator. For example, in every language I have ever touched, other than JavaScript (and I have written thankfully little JavaScript), || only operates on integral types, where the behavior is pretty usual... but I will still ding people in code review who use short-circuiting for complex control flow (i.e. anything more complex than ptr != nullptr || ptr->x_ != 0).

So could the first; the callee is, in any reasonable overload mechanism of operator||, allowed to disable short-circuit behavior.

3 Likes

Well then,I suppose the mechanism proposed in the first comment is not "reasonable".

I don't see why you would want to be able to disable short circuiting when overloading short circuiting operators,short circuiting is the only reason why you would use them in the first place.

2 Likes

I don’t see how being able to disable short circuit on a short circuit operator is “reasonable” . Mine is a strict definition of short circuit version of “bitor”

1 Like

I think this is the proper desugaring:

A || B
A && B

becomes

let a = A; if(a.is_truthy()) {a} else {B}
let a = A; if(!a.is_truthy()) {a} else {B}

Why?

It seems to me the opposite is true, i.e. || and && must always short-circuit.

2 Likes

Short-circuiting is driven by is_truthy; the callee may implement this as identity true or false if they wish.

The typical other scheme, which is is a synthetic closure thing where || has type fn(A, impl FnOnce() -> B), has the same problem, since the callee can choose to call the second function unconditionally.

1 Like

To be more precise, I think A && B and A || B must always have a value of either A or B since that’s what JavaScript does and it seems the only reasonable thing (also given that bitor/bitand may not be defined on the type), so calling the bitwise operators is wrong and together with the short-circuit requirement leads to the desugaring in the previous comment.

Otherwise with the desugaring in the OP, we have that 3 || 4 has value 7, while it obviously should have value 3 since 3 is nonzero.

1 Like

In C, 3 || 4 evaluates to 1 (since there is no boolean type before C11) and true (as in, a bool) in C++. I don't think that "must be A or B" is a perfect interpretation (if we plan to go whole-hog, that is...), nor do I think JavaScript's semantics are a good place to go looking for inspiration.

3 Likes

I think it should be because that is what it means