None and Err coercion operator

Add None and Err coercion operator

Basically it will be like unwrap_or()

I really like in kotlin the elvis operator: ?:

It allows you to write code like this:

let string = optional_string ?: "";

Kotlin's ?: operator is more similar to .unwrap_or_else(|| ...). Except that it also allows control flow on the right, e.g.

val string = optional_string ?: throw SomeError()
// or
val string = optional_string ?: return 0
// or
val string = optional_string ?: continue

Rust has let-else for this:

let Some(string) = optional_string else {
    return 0
};

I really like how concise and powerful ?: is. JavaScript's ?? operator is similar, but does not support control flow keywords.

However, it will be hard to justify an elvis operator in Rust, because unwrap_or_else and let-else already exist, so there isn't a strong incentive to add another syntax, which will make Rust (which is already a complex language) even more complex.

6 Likes

I gues let-else is basically the same thing and so is unwrap_or_else() but ?: is really concise and faster write

I doubt we'll get an operator for Option only, so one should consider how this could work with Try types (or perhaps with all types). But the naive desugaring for Try types...

    // expr ?: ...
    // becomes
    match (expr).branch() {
        ControlFlow::Continue(happy) => happy,
        ControlFlow::Break(_) => ...,
    };
let string = fallible_string ?: ""; // Error silently discarded :-(

...results in throwing out the Residual -- for example, throwing out the Err variant of a Result -- which is not something that should be encouraged by being easier to type.[1]

So if this idea goes anywhere, there should be a desugaring that "catches" the Residual.

The existing Residuals of Try types are enums with some variants omitted (by filling them in with Infallible/!). This plays nicely in combination with exhaustive_patterns when there's only one variant left.

Consider a desugaring like

    // expr ?: match [ident] { ... }
    // becomes
    match (expr).branch() {
        ControlFlow::Continue(happy) => happy,
        ControlFlow::Break(residual) => match [ident @] residual {
            ...
            // (residual is not nameable except via ident)
        }
    }
    let string = optional_string ?: match { None => "" };
    let string = fallible_string ?: match { Err(e) => {
        eprintln!("Wha? '{e}'  Continuing on anyway...");
        ""
    }};

Still pretty clunky. But as a special case, instead of match you can supply a single pattern (which would result in an error if the pattern doesn't cover all inhibited possibilities):

    // expr ?: pattern => ...
    // becomes
    match (expr).branch() {
        ControlFlow::Continue(happy) => happy,
        ControlFlow::Break( pattern ) => ...
        // Same thing effectively:
        /*
        ControlFlow::Break(residual) => match residual {
            pattern => ...
        }
        */
    }
    let string = optional_string ?: None => "";
    let string = fallible_string ?: Err(e) => {
        eprintln!("Wha? '{e}'  Continuing on anyway...");
        ""
    };

Silent discards are still too easy, but perhaps this could be a lint.

    let string = fallible_string ?: _ => "";

Why not add a "type Break" to Try or such instead?

  • Because nothing in the current design says that your Residual has to have only have one possibility.
    enum Example<T, U, V> { Happy(T), Sad(U), Eh(V) }
    // ...
    type Residual = Example<Infallible, U, V>
    
  • Because spelling out Err(e) instead of just having e (for example) communicates that you're handling an error much better

  1. let else also has this weakness to some extent, but it's much easier to spot a let Ok(_) = ... else in review because the Ok must be spelled out (and the must-diverge rule knocks out some temptations too) ↩︎

I think it would be better if had another trait called TryOr

trait TryOr {
    type OkType;
    fn try_or(self, or: Self::OkType) -> Self::OkType;
}

impl<T, E> TryOr for Result<T, E> {
    type OkType = T;

    #[inline]
    fn try_or(self, or: Self::OkType) -> Self::OkType {
        match self {
            Ok(ok) => ok,
            Err(_) => or,
        }
    }
}

result ?: … => result.try_or(…)

Here's an old related thread: Something for coalescing; aka generalized/improved `or_else`

That still just throws away the error value.

but the point is to get rid of the error

The ?: was also discussed in this thread: Elvis operator for Rust

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.