Pre-RFC: Overload Short Curcuits

So far I agree with all except

Some(2) || Some(3) should produce Some(2)

Sebastian Malton

1 Like

Basically I want the following conversions via From / Into:

true => Some(()) => Ok(())
false => None => Err(())

or the other way around:

Ok(T) => Some(T) => true
Err(E) => None => false

As long as these three works, and we have something we can extend to your own types, it matters less if we use Into<bool>, Into<Option> or Into<Result>. Maybe even std::ops::Try.

Good reply, this is exactly the kind of discussion I'd hoped to spark -- way more interesting that defining traits.

You have an associativity assumption in there. None || None || 2 could be None || (None || 2), at which point None || None on its own never needs to work. (Though it still would, because None || None can be None::<Option<T>> || None::<T>.)

Then Some(1) || Some(2) || Some(3) || 4 compiles fine.

Edit: Hmm, Operator expressions - The Rust Reference says that || is left-to-right :frowning: I wonder if that's actually observable anywhere today?

It would be if you are doing effectful actions behind a ||, but that is code smell

false || { println!("Hello "); false } || { println!("World!"); true }

// or 

check || return

Yes, all arithmetic operators have the same associativity precedence, so it would be bad to change || to a different one.

Both parenthesizations of that have the same output, though: Rust Playground

That said, I think this thread, on re-reading, has convinced me that Some(1) || Some(2) ought to work and produce Some(1). (Unlike in the macro in my previous thread.)

Anyone have examples where the values from the two sides should be combined on some way? (Maybe @Nokel81 or @bugaevc, who posted traits with an or of some sort between the sides.)

1 Like

Huh, well I guess I should check these things before I say things

If I understand what you're asking — I've written an impl that gives you Option<A> && Option<B> -> Option<(A, B)> in the playground I've linked above, and one for Result<T, E> || Result<T, F> -> Result<T, (E, F)> in response to @timvermeulen; but they're just examples of what's possible when the output type doesn't equal Self and the answer includes a value from self (not just depends on it) — if you ask me, I'd rather not have libcore provide those impls.

The difference between Option<((A, B), C)> and Option<(A, (B, C))> is also an example of when associativity matters.

6 Likes

That it does unless we can define a flattened tuple.

One useful impl we could also have is:

let opt: Option<T> = ...;
let res: Result<T, E> = opt || Err(e);

this is currently known as ok_or() / ok_or_else().

This would only work if the right hand side is an Err variant; my first idea was to write the types as Option<T> || Result<!, E> -> Result<T, E>; but the compiler rightfully points out that together with Option<T> || T -> T this makes Option<Result<!, E>> || Result<!, E> ambiguous. The workaround is to tie the type of the Ok variant to T somehow while keeping it uninhabited:

struct PhantomNever<T> {
    phantom: PhantomData<T>,
    never: !,
}

impl<T, E> LogicalOr<Result<PhantomNever<T>, E>> for Option<T> {
    type Output = Result<T, E>;
   ...
}

Or, we could just allow generic Option<T> || Result<T, E> -> Result<T, E>, but that would mean always throwing out the Ok() variant of the RHS Result.

Thoughts?

I think that this is fine

Should we allow the other way? I don’t feel strongly either way

Sebastian Malton

How? Do you want Result<T> || Option<T>? What would it return? Currently, we don’t have any functions that do this right now, so there isn’t any precedence for it.

I’m guessing Result<T,E> || Option<T> = Option<T> corresponding to res.ok().or_else(opt)

But how is this significantly better than just doing res.ok() || opt, which is already quite terse. I don’t see any reason to add this. Anyways, I don’t think we need the other way, because it doesn’t gain all that much.

2 Likes

I was thinking like that but I don’t know if it makes enough sense to have explicitly instead of having people just call res.ok() || opt

Sebastian Malton

My biggest problem with this operator is that the error value on the left hand side is always ignored:

Ok(x) || Ok(y) = Ok(x)
Ok(x) || Err(y) = Ok(x)
Err(x) || Ok(y) = Ok(y)
Err(x) || Err(y) = Err(y)

We should thus extend the implementation for result to

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

Even if we did this, we are implicitly ignoring a value, Maybe we would rather describe this relation as res1.ok() || res2, so that it is explicit that we are ignoring a value

1 Like

No the error on the right hand side would be ignored.

That seems even more arbitrary to me. My version coresponds to:

match $expr1 {
    Ok(v) => Ok(v),
    Err(_) => $expr2,
}

While what you are proposing would correspond to:

match $expr1 {
    Ok(v) => Ok(v),
    Err(e) => {
        match $expr2 {
            Ok(v) => Ok(v),
            Err(e) => Err(e),
    },
}

This requires holding on to the previous error, as well as performing an extra branching

2 Likes

I agree with

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

It is simpler to implement, and it is semantically more clear. If we have an Ok we return it, otherwise return the right result.

I think if I tried to spell out the behavior I’d intuitively expect for Option and Result, I’d end up giving the two types diametrically opposed behaviors that can’t reasonably be merged.

Maybe this is just me, but my intuition is that for Result, the Err() case is an error, that its existence represents a non-happy path in control flow and something you do not want to “swallow” by mistake. While with Option, there’s usually nothing erroneous or unhappy about None. Similarly, when faced with a collection of Results, I typically want to either early return with the first Err or proceed with all Oks, while a collection of Options I typically want to either filter out all the Nones or just map them over and over. This sort of thing leads my brain to tell me that Some(t) || None should be Some(t), but Ok(t) || Err(e) should be Err(e), and I don’t think it’d make sense to do both of those, because then result.ok() || None is legal code and my brain just goes ??? in that case.

Am I being super weird, or does anyone else feel a similar tension of intuitions?