Pre-RFC: Overload Short Curcuits

Integers shouldn't be usable usable with short circuiting operators anyway,those operators should be implemented on types which have an and_then and/or or_else methods (or equivalents) like Option and Result.

Yes,seems like is_truthy should either be in its own trait or not be the way short-circuiting is done.

trait IsTruthy{
    fn is_truthy(&self)->bool; 
}

By making it a separate trait you can't make the value change its truthyness for short-circuiting when using either || or &&,truthyness has to behave the same for both.

It could make sense to make || act as or_else. The problem is that or_else isn’t always a zero-argument callback.

With Result, or_else gives the error to the closure. This can’t be done with a || right hand operand, unless it’s a closure, in which case you’re just making it different rather than theoretically better.

It’s worse with &&/and_then, where operating on the previous “positive” type’s content is the 99% case, rather than or_else which doesn’t have one for Option and sometimes you ignore it for Result.

And in either case, we have a trait already that would cover the conversion to positive/negative that’s already used: Try. With try blocks, a || b is try { a?; b }. && would be, I suppose, monadic do notation.

result || continue is something that’s decently often wanted in certain niches, similar to result? effectively being the equivalent of result || return. I think the current option would be postfix macros and an .or_else! macro that works on Try types, but if this integration can be done with || gracefully, in a way that doesn’t read like it’s just trying to port JS mechanics to Rust.

You could also say that if a { true } else { b } is "extremely clear", but yet we have a || b.

Note that you could also write that as

if let e @ true = a { e } else { b }`

so the extension of a || b for Option to

if let Some(e) = a { e } else { b }

seems like a perfectly reasonable analogue.

Why is that? Is there something rust-specific that would make things worse in rust than elsewhere? In C#, at least, I find ?? clearer and not bug-prone -- and that's even with it starting to get used in intentionally-short-circuiting situations like

this.foo = foo ?? throw new ArgumentNullException(nameof(foo));

If anything, I might guess the opposite, as putting something into a closure means that you lose much of the move-tracking and NLL goodness of rust, so having the compiler understand the control flow here might make it less error-prone than or_else. Not to mention that we know that .or(foo()) vs .or_else(|| foo()) is already a source of bugs for people.

(And, come to think of it, foo || panic("blah") gives a nicer callstack than foo.expect("blah").)

5 Likes

Yes, see my thread that was linked above for a larger discussion of how we could do this with Try.

Note that try { a?; b } is a && b -- not a || b -- because it short-circuits on false.

I don't think this is the right desugar, because it's weird with Option. For None || "hello" you'd have to define bitwise-or between Option<_> and &str, which is a weird operator to have. And it doesn't work anyway, because you meant &self in the trait.

But you really want to destructure the input by move, not just have an inspector. Following the Try example, you might consider fn into_option(self) -> Option<Self::Success>; or similar.

3 Likes

Yeah, I find the or_else vs or a constant problem that I have to look up which to use.

Well I think that None || "foobar" is a weird use case. I would be more likely to do between two variables both of options.

That’s not my experience in C#. I find it far more common that I’m doing foo ?? 0 or similar to go from a Nullable<int> to an int.

Currently it is so nice that && and || are logical operations, i.e. you know that their arguments as well as the result are booleans. I think losing this sort of clarity for the sake of cleverness is definitively a net negative.

Any time I come across a language that has “truthiness” and “falsiness”, I almost automatically read “sloppiness”. There’s no universal and obvious way of defining what maps to true and false, and I’d rather see the actual conditions and conversions written out in the code instead of having to vaguely remember (or worse yet, synthesize on the spot based on my potentially wrong assumptions and partial memories) all the rules for a particular type.

JavaScript’s overloading of && and || is a mistake, but it’s nevertheless a popular one because people are just tempted to code-golf regardless of whether it’s warranted. Let’s actually learn from past mistakes of language design this time. (I guess the same applies to the “null should be the Default for raw pointers” argument in another recent thread.)

It’s the same kind of attitude that DrXor mentions w.r.t. if (ptr) versus if (ptr != nullptr). This just shouldn’t be a question. A conditional should depend on a boolean, no less, no more. If you want to compare a pointer against a particular value, you can get it with an equality operator. If you want to see if an optional is Some, you can have that information from is_some(). If you want to chain them, yes, I’m going to say it, you can use or_else() and rewrite your code in a way so that a trivial closure doesn’t get in the way. I’d argue that if introducing a closure there is a problem, then you probably have greater cleanliness issues with the piece of code in question.

4 Likes

So what if it looked like this then, to make it clear that it's not a generic "is truthy" / "bool coercion", and additionally to make it possible to return custom values when short circuiting:

enum ShortCircuit<S, L> {
    Short(S),
    Long(L),
}

trait LogicalOr<Rhs = Self>: Sized {
    type Output;

    /// Decide whether the *logical or* should short-circuit
    /// or not based on its left-hand side argument. If so,
    /// return its final result, otherwise return the value
    /// that will get passed to `logical_or()` (normally this
    /// means returning self back, but you can change the value).
    fn short_circuit_or(self) -> ShortCircuit<Self::Output, Self>;

    /// Complete the *logical or* in case it did not short-circuit.
    /// Normally this would just return `rhs`.
    fn logical_or(self, rhs: Rhs) -> Self::Output;
}

then A() || B() desugars to

match A().short_circuit_or() {
    ShortCircuit::Short(res) => res,
    ShortCircuit::Long(l) => l.logical_or(B()),
}

This way it'd be possible to have

impl<T> LogicalOr<T> for Option<T> {
    type Output = T;
    ...
}

(currently known as .unwrap_or()), where if it short-circuits, the output type is T, not Option<T>; and it also works for non-Copy types — in your proposal, self would be consumed by is_truthy(), in mine it is (typically) returned back wrapped in ShortCircuit::Long().

Here's a playground with LogicalOr, LogicalAnd, some impls for bool and Option, a logical! macro to perform the desugaring, and a demo.

4 Likes

I agree that an if statement should always be a boolean, no matter what. That is good design.

And it is nice that && and || are logical operators but I disagree that forcing people to use closures is a good solution. It also seems to be possible (with the upcoming try blocks) to do "&&" but not "||".

I agree, but I don't see why that should factor here. Each operator does not have to implemented on each type pair.

I do think that it maps nicely onto result and option though and I don't feel that it is too strange.

How would you feel adding the || operator just to those types being usable in the following cases:

  1. Option<T> || Option<T>
  2. Option<T> || <T>
  3. Result<T, E> || Result<T, E>
  4. Result<T, E> || <T>

I would expect such a trait to look something like this:

trait LogicalOr<RHS = Self> {
    type Output;
    fn logical_or(self, rhs: impl FnOnce() -> RHS) -> Self::Output;
}

Then the implementing type has complete control over how the short-circuiting is done.

@timvermeulen This is no better than just using or_else on Option and Result. I think that @bugaevc’s proposal is the best designed, so far as it makes how the short circuiting works in it’s design.

1 Like

You wouldn't call it directly, a || b would call it for you. I was only commenting on what the trait corresponding to || could look like.

Yes, but it still would suffer from all of the problems of closures, but now there is a hidden closure (even worse).

The desugaring would be

a || b

becomes

LogicalOr::logical_or(a, || { b })

Note the closure, this means that

a || return

would be the same as

LogicalOr::logical_or(a, || { return })

Which never returns from the calling function. This is a breaking change. contrived playground

So actually, any version of this proposal that makes a closure can’t work. The short circuiting must happen in the desugaring.

3 Likes

Ah, I missed that logical_or simply wouldn’t be called in the case of a || return. Understood.

I think that trait should ideally get another Intermediate / Falsy / Empty / Failure associated type though, to be used in the Long variant of the return value of short_circuit_or. For bool and Option it could be (), but other types may want to pass some value other than self to logical_or.

That's not the problem. I have taken the liberty to playground the desugaring of your LogicalOr, and your desugaring doesn't compile. playground for the desugaring for false || return. Currently this compiles and unconditionally returns from the function.

Yes, I understand, I was saying that I didn’t realize the alternative design would support early returns. I was talking about their logical_or, not mine, sorry for the confusion.

1 Like

Can you elaborate on exactly what you mean by that, and whether that's what this proposal actually is? For example, I agree that "0" being false in bool context is bad, but that was never proposed in this thread.

But Option and Result already have methods using "or" and "and", and thus they've already defined their mapping to true and false: Some and Ok map are truthy, whereas None and Err are falsey.

So just like how I don't find a + b any more confusing than a.add(b), I don't find a || b any more confusing than a.or_else(|| b). (In both cases I, in fact, prefer the operator.)

This seems like a strawman to me, because there's no proposal to allow if Some(3) to compile here.

6 Likes

This remind me to think Ruby’s "block"s. Ruby’s lambda behaves like Rust’s, but it also have another form called "block"s, which flow control can escape the block and leak to the calling environment.

Ruby does not have life time, so if we want to do something in Rust we need to think again. For example, we might want to limit such "block"s to have strict lifetime that is shorter than the calling function, to avoid the difficulty that someone attempt to “break” to a function already returned.

Yes, that would work, but I’m not sure if it’s a good idea to have blocks in the first place. I can see the use in things like iterators, and other adapters, but they would be much harder to use correctly (especially around unsafe) so I don’t know if they are worth it.

1 Like