Pre-RFC: Overload Short Curcuits

I believe the intention is to keep ||'s short-circuiting behavior, in which case the Err(e) expression in Ok(t) || Err(e) would never be evaluated. Only the Ok(t) expression would be evaluated.

Put another way, imagine two functions: foo() and bar(). Both return a Result<T, E>. The expression foo() || bar() would evaluate foo() first, and if it's Ok(_), the expression short-circuits and bar() is never evaluated.

The only way for Ok(t) || Err(e) to result in Err(e) is if || loses its short-circuiting (unless you flip the short-circuiting behavior and have Err(1) || Err(2) short-circuit such that only Err(1) is evaluated (and returned), but that goes against Result's or method and I believe people are trying to make || consistent with Option's and Result's or_else methods).

3 Likes

What it sounds like is for Result you want && more often, rather than ||. (One possible version of && is try {(foo()?, bar()?)}.)

2 Likes

Wouldn't None || Ok(x) have to evaluate to Ok(x)? Which would mean the Ok variant of the RHS isn't always thrown out.

1 Like

No, that's not just you, that's how Results/Options are supposed to be used, but

I'd say

Some(t) || None -> Some(t)
Some(t) && None -> None
Ok(t) || Err(e) -> Ok(t)
Ok(t) && Err(e) -> Err(e)

Right; don't know what I was thinking. Let's just allow Option<T> || Result<T, E> -> Result<T, E> then.

3 Likes

If we’re adding

Option<T>    || Result<T, E> -> Result<T, E>
Result<T, E> || Option<T>    -> Result<T, E>

then I really think it’s best for Result<T, E1> || Result<T, E2> to return Result<T, (E1, E2)>, even though that isn’t in line with Result::or, which ignores the first error. My reasoning is that it would give you the option to keep both errors while still letting you ignore the first error implicitly with res1.ok() || res2. It’s also more consistent with

Result<T1, E> && Result<T2, E> -> Result<(T1, T2), E>
2 Likes

I agree that keeping information is probably a good thing. But I am hesitant to agree with you on this case because of the current situation surrounding tuples.

This would mean that

Result<T, E1> || Result<T, E2> || Result<T, E3> -> Result<T, ((E1, E2), E3)>

Which is not very nice indeed. It also doesn't play very nice with ?. Lastly, I would say that it makes the most sense to restrict Result<T1, E1> | Result<T2, E2> to the case where T1 == T2 && E1 == E2 because of playing nice with other parts of the language.

This might be true but I also don't think that this should be the case. The equivalent code of

try { a?; b? };

Just ignores the first Ok(...) value.

But since res1.ok() || res2 will ignore the first error, this shouldn't be too much of an issue. But I think that we should be consistent with the current Result::and and Result::or so

Result<T, E1> || Result<T, E2> == Result<T, E2>
2 Likes

Makes sense. Since the discussion has slowed down I believe making a preliminary draft would be in order so I will start on that. Incorporating all comments here.

1 Like

So then I guess the idea is also to have Option<A> && Option<B> return Option<B> instead of Option<(A, B)>? This seems way less useful to me than to keep both values. I know that’s how Option::and works, but does anyone ever use that method? Conditionally unwrapping two Options is a very common operation.

I've been wondering through this if && is just better done with ? for Result/Option, and shouldn't be overridden. Then you have both try { a?; b? } and try { (a?, b?) } depending which you want.

Actually, a crazy thought: De Morgan. (AKA we have an implied requirement for consistency between the trio of ! and || and &&.) If !Result<T, E> ==> Result<E, T>, then Ok(a) && Ok(b) => !(!Ok(a) || !Ok(b)) => !(Err(a) || Err(b)) => !Err(b) => Ok(b) --- so yes, && would drop the first success value if || drops the first error :smiling_imp:

2 Likes

I don't think anyone was disagreeing with this :slight_smile: Having one of those drop the lhs (in the case of no short-circuit) and not the other would definitely be weird. But having || and not && might also be undesirable.

Another question: what should Result<T, E> || Option<T> return, if it should be allowed at all? If we consistently want || to ignore the first error, then it should return Option<T>. But then we have

Result<T, E> || Option<T>    -> Option<T>
Option<T>    || Result<T, E> -> Result<T, E>

I really don't like the asymmetry of this.

Like I've consistently argued with ?, I think mixing should just be disallowed.

If one wants the former one can do a.ok() || b, and for the latter one can do a.ok_or(()) || b since the error is thrown away regardless. (Though I suppose the or-then-|| is suboptimal in the latter.)

The idea behing mixing them was to make opt || Err(err) an alternative to opt.ok_or(err). I really like the way opt || Err(err) looks, and it’s arguably simpler because you don’t have to worry about ok_or vs ok_or_else.

1 Like

In the light of all these debates, I think this kind of overloading would make matters even worse, because if there’s no symmetry between the operators and .or() / .and() nor between the behavior of the operators defined on Option and Result, then the only thing this leads to is dialect-like divergence of the language, and that is anything but desirable. Furthermore, the resulting inconsistency makes learnability worse, so it doesn’t help with the much-touted ergonomics issue either. I don’t think at all that the gained minor terseness is worth this hassle.

6 Likes

Can you clarify what you mean by a lack of symmetry between the behavior of the operators defined on Option and Result?

Even if we don't end up overloading || and && for Option and Result, making them overloadable for user-defined types still seems like a win.

2 Likes

If one of them throws away one of the types and one of them instead yields a tuple of both types, that’s an asymmetry.

To the rest: allowing the overloading of these operators still doesn’t seem to be an obvious win to me, see my other arguments and concerns above.

The least crazy part in my opinion, having !(_: Result<T, E>): Result<E, T> (expressed with generic type ascription) looks useful on its own. It would certainly make || interesting to write (replace try with move || Ok(..) depending on completed RFCs):

!(try { !a?; !b? })
1 Like

I would not be in favour of this because it both looks very “golfy” and doesn’t make a lot of sense. It also doesn’t fit into a nice mental model because !Option<T> doesn’t really work very well either.

That's indeed an asymmetry, but I don't think anyone is suggesting that the Option overload should throw away a value but not the Result overload, or vice versa. That would indeed be weird. Not discarding any values or always discarding the lhs value would be consistent.

1 Like

That's because you expect ! to be the boolean algebraic ! and hence !!Option<T> should be the original Option<T>. When the intermediate type of !Option<T> must also be Option<T> then the impl could be made to work for T: From<()> + Into<()> or some other zero-sized marker type but how much sense is in that?

However, I don't think it is necessary for those to work the same way and I also don't find the example on Result<T> to be significantly less readable than the proposed one for &&. Maybe that's just personal preference. Also, in my eyes the equivalent version of Option<T> would be a new type which is not the 'bottom'-semilattice extension but the 'top'-semilattice (call it Complete<T> without regards to bikeshedding) and then

enum Complete<T> {
    Full,
    Part(T),
}

!Option<T> -> Complete<T>. !Complete<T> -> Option<T>. (None to Full and Some to Partand the other way around.)

What does this have to do with short curcuiting? Because apart from having inverted Ord implementation on Complete, its Try could have the semantics of being successful only in the Full case and then we would have the same possibilty for || for Option: !(try { !a?; !b? }) is what's proposed as a || b.

In particular, we might have

impl<T> Try for Complete<T> { 
    /// Note: Don't create a dedicated type.
    /// The expression `self?` feels nicer having unit type.
    type Ok = ();
    type Err = T;
    fn into_result(self) -> Result<(), T> { 
        !(!self).ok_or(()) // Ok, this is a bit satirical. Match is nicer.
    }
    fn from_error(err: T) -> Self {
        Part(err)
    }
    fn from_ok(v: ()) -> Self {
        Full
    }
}
5 Likes