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").)
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 -- nota || 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.
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.
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.
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:
@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.
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.
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 Resultalready 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.
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.