Something for coalescing; aka generalized/improved `or_else`


#1

It was noted in the await syntax thread that Option::or_else mixes poorly with await (and ?): https://github.com/rust-lang/rust/issues/57640#issuecomment-457518407

Since one can think of ? as and_then, would it make sense for there to be some kind of standard macro or operator for or_else? Like how C# has the null coalescing operator ?? (which spawned the or_else conversation) and SQL has the COALESCE function.

As I’ve been spending a bunch of time with ops::Try lately, I realized we could do something like this:

#![feature(try_trait)]

macro_rules! coalesce {
    ($a:expr) => ( $a );
    ($a:expr , $($c:expr),+) => (
        if let Ok(v) = std::ops::Try::into_result($a) {
            v
        } else {
            coalesce!( $($c),+ )
        }
    );
    ($($a:expr),+ ,) => (coalesce!($($a),+))
}

fn main() {
    assert_eq!( coalesce!(4), 4 );
    assert_eq!( coalesce!(Some(4), 2), 4 );
    assert_eq!( coalesce!(None, None, None, 2), 2 );
    assert_eq!( coalesce!(None, None, Some(3), 2), 3 );
    assert_eq!( coalesce!(Err(4), 2), 2 );
    assert_eq!( coalesce!(Some(1), unreachable!()), 1 );
    assert_eq!( loop { coalesce!(Err(()), break 4) }, 4 );
    assert_eq!( loop { break coalesce!(Some(1), break 4) }, 1 );

    assert_eq!( coalesce!(4,), 4 );
    assert_eq!( coalesce!(Some(4), 2,), 4 );
}

https://play.rust-lang.org/?version=nightly&edition=2018&gist=a6dc7ae213e42e4882781046146ad3dd

Thoughts?

Edit: Adding some links to related things:


Proposal: Option::cow_or_else
#2

If “trait provided postfix macros” were a thing, this’d probably be a good thing as (fake syntax)

macro Try::or_else!($self, $($f:tt)+) {
    if let Ok(v) = $crate::ops::Try::into_result($self) {
      v
    } else {
        $($f)+
    }
}

fn main() {
    assert_eq!( 4, 4 );
    assert_eq!( Some(4).or_else!(2), 4 );
    assert_eq!( None.or_else!(None).or_else!(None).or_else!(2), 2 );
    assert_eq!( None.or_else!(None).or_else!(Some(3)).or_else!(2), 3 );
    assert_eq!( Err(4).or_else!(2), 2 );
    assert_eq!( Some(1).or_else!(unreachable!()), 1 );
    assert_eq!( loop { Err(()).or_else!(break 4) }, 4 );
    assert_eq!( loop { break Some(1).or_else!(break 4) }, 1 );
}

I definitely think that this operation is useful for Rust (I mean, we have it in method form for both main Try types), and that generalizing it like this is cool. The continuing comma support for the standalone coalesce! is nice but probably not desired for a postfix macro or operator.

Actually, what operator could this “upgrade” to? ?? is ? ? and should probably stay that (because Result<Result<_,_>,_> exists) (though I could be convinced otherwise if res.flatten()? could work for two distinct error types here). ?: is almost definitely required to stay ? followed by type ascription. What other operators remain that would make sense for this? @#$_&-+()/*"':;!?,.~`|•√π÷׶∆£¢€¥^°={}\%©®™✓[]<> is a dump of all the main symbols on my phone keyboard (GBoard) (which definitely has more than any desktop keyboard), and none of those seem super appropriate.

[[[ as an alias for ? when]]]


#3

I think the only operator that would make sense is ||. It’s already lazy, as this one would be, and currently isn’t overloadable so it wouldn’t break anything. (And you can imagine a impl Try for bool where b? short-circuits on false which would be perfectly consistent with this definition, though I suspect we don’t actually want to allow ? on bool.)

OTOH, maybe just the macro would be enough. A bunch of the try{} discussions have talked about “what if I just want to return the first success?”, where it might be good not to have that use try/?, but instead use something like

coalesce!(
    fetch_from_primary(),
    fetch_from_secondary(),
    Err(NotFoundInEither),
)

#4

In parallel, an operator && can also be defined in the same way. A proposal would be

// in std::ops
// Operator `||` 
// a || b desugar to a.or(||b)
trait Or<Rhs=Self> {
    type Output;
    fn or<F: FnOnce() -> Rhs>(self, f: F) -> Self::Output;
}
// Operator `&&`
// a && b desugar to a.and(||b)
trait And<Rhs=Self> {
    type Output;
    fn and<F: FnOnce() -> Rhs>(self, f: F) -> Self::Output;
}

Now or_else is || and and_then is &&. So then we can move && and || out of the “magical operators” set.


#5

Only problem with this is that it may be blocked on precise closure captures because we are hiding a closure in there. Otherwise I think this is a great idea!’

edit: as @scottmcm , this won’t work due to backwords compatibility issues.


#6

I don’t think anything involving closures will work any time soon, since stuff like this is legal today:

fn bar() -> bool { false }
fn foo() -> u32 {
    bar() || return 1;
    2
}

And that return can’t be put in a closure today.

(The Try-and-if desugar doesn’t have that problem, as the macro demonstrates.)


#7

We can do a little bit CPS in this case (require never_type to land first):

// in std::ops
// Operator `||` 
// a || b desugar to a.or(|_|b, _compiler_generated_label)
// a || return c desugar to a.or(|r| r(c), _compiler_generated_label)
trait Or<Rhs=Self, ReturnType> {
    type Output;
    fn or<
        F: FnOnce(Return) -> Rhs, 
        Return: FnOnce(ReturnType) -> !,
    >(self, f: F, r: Return) -> Self::Output;
}
// Operator `&&`
// a && b desugar to a.and(|_|b, _compiler_generated_label)
// a && return c desugar to a.and(|r| r(c), _compiler_generated_label)
trait And<Rhs=Self, ReturnType> {
    type Output;
    fn and<
        F: FnOnce(Return) -> Rhs,
        Return: FnOnce(ReturnType) -> !
    >(self, f: F, r: Return) -> Self::Output;
}

the real problem I saw, is that the compiler have to find a way to decide whether to put a move keyword before the closure.


#8

This is what I’ve tried to resolve in [Pre-RFC] Elvis/coalesce + ternary operator thread. Currently I work on RFC for this syntax


#9

Let’s not put it by default:

  1. It is uncommon for conditionnally executed code to take ownership of something ;

  2. And when it happens, you can always type hint using a shadowing explicitely typed let binding within the closure

Example:

fn main ()
{
  let s = String::from("foo");
  ::std::thread::spawn(|| {
    let s: String = s;
    println!("{:?}", s);
  });
}

When taking ownership of something is required for a closure, the compiler is suprisingly good at figuring that one out. The one exception is ::std::thread::spawn since the other thread may just mutate or read the captured environment, and the reason move is needed is because of the 'static lifetime bound on the closure (not its FnOnce-ness). It doesn’t look like the kind of problem involved in this or_else sugar scenario


#10

In fact, my original design should work even for early returns. It just require the compiler to generate a special entry for return or break or continue, and let the generated function body jump directally to it, leaving an expression typed !.

i.e.

a || b desugar to a.or(b)
a || return c desugar to a.or(|| _compiler_return_glue(c))

As _compiler_return_glue(c) is !, the above should type check.


#11

The reason why false || return 1 works is because return 1 has the type ! (never type). So you can also do false || any_function()

where any_function is defined as so,

fn any_function() -> ! { /* your code here*/ }

and it would also compile. Now, your desugaring cannot handle this case, and it must because the never type is getting stabilized.


note that this compiles as well: false || loop {}


#12

Why? It should work as any_function is specified by the user, so it should be captured in the closure, and so there wouldn’t be any problems!

This works as rocks! false || loop {} becomes false.or(|| loop{}). If the closure didn’t call, nothing happen. If it is called, infinite loop.


#13

Right now it always goes into an infinite loop. Edit: when i checked this yesterday I don’t think I properly checked it, so it is incorrect.


#14

Yes, that is true, I didn’t state my problem clearly.

false || {
    /* your code here */
};

where the block evaluates to !, and uses return, continue, or break with non-trivial control flow. There wouldn’t be a way to desugar this. Therefore this change would require a breaking change.


#15

false.or(|| loop{}) is also always goes into an infinite loop, if or is the usual definition. But true || loop {} or true.or(|| loop{}) will not loop at all.

I already proposed my solution.

a || return b => a.or(|| __return_label(b))

a || break b => a.or(|| __break_label(b))

a || continue => a.or(|| __continue_label())

Only the compiler can generate those labels, and those instructions are just jmp to the label.