[Feature Request] Unwrap nested options when return type is Result

EDIT: Look at @jkugelman reply and the conversation following it

Would be amazing to have this operator similar to the one in JS ??. The signature looking something like:

??: Option<T> ?? T -> T
??: Result<T, E> ?? T -> T

Where if the LHS is good, Some or Ok in this case, then it short circuits. Otherwise it returns the RHS. The implementation would like something like:

match Try::branch(LHS) {
    ControlFlow::Continue(c) => c,
    ControlFlow::Break(_) => RHS
}

Wouldn't need to to actually be ?? as this could be confusing. But, it would be nice to have to be ?? as it is in popular languages like JS, TS, and Dart as the nullish coalescing operator, which is quite similar to the signature given above.

Thanks to @TheMissingPiece, @danielrab on the discord in the #contribute channel for their discussion. Discord discussion

This was proposed here not long ago: How about a new operator?

It seems a lot of the discussion is around ambiguity. We could easily use any other character(s) to denote this operation. But we could just say ?? has the lowest precedence

Adding ?? is not an obvious improvement to Rust owing to how different the ergonomics around Option and Result are compared to null handling in other languages:

  • Everything is not nullable by default. Defensive null guards aren't a thing.
  • There are many options to handle Nones and Errs: the ? operator, if let, match, a variety of combinator methods on Option and Result, the soon-to-be-stabilized let else, etc.
  • We already have unwrap_or and unwrap_or_else.

Can you give some examples of code that would be improved with this operator? It would help ground the discussion if we could look at some concrete examples, particularly ones pulled them from real code out in the wild.

7 Likes

Related proposal: Allow Overloading || and && RFC (postponed) internals thread.

1 Like

Ok, having gone through and tried to find some examples I've realized where I think this would actually be the most useful. Let's say you working with some api, in the examples below I'll be using the NYC MTA api because it is full of options. Iirc the reason it is mostly options is that they don't have like different messages that are sent. Just a single message and you have to do a bunch of option checking to see if something actually exists. Let's have our main function be something like:

#[tokio::main]
async fn main() -> Result<(), SomeErr> {
    let https = HttpsConnector::new();
    let client = Client::builder().build::<_, hyper::Body>(https);
    let req = Request::builder()
        .method("GET")
        .uri("https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs")
        .header("x-api-key", "api-key")
        .body(Body::from(""))?;

    // Await the response...
    let resp = client.request(req).await?;
    let data = hyper::body::to_bytes(resp.into_body()).await;
    let feed_message = nyct::FeedMessage::decode(data?);
    let feed_entity = feed_message?.entity;

   // ...... other stuff ........
    Ok(())
}

The structure of a feed_entity is basically options all of the way down but here is a gist with all of their types.

Let's try and grab some of the members of the inside more specifically I want the stop_id, arrival_time, and departure_time of all of the stop_time_updates printed to the console. If one of these doesn't exist print the default value.

for entity in feed_entity {
        if let Some(trip_update) = entity.trip_update {
            for stop_time_update in trip_update.stop_time_update {
                print!(
                    "stopId: {} ",  
                    stop_time_update.stop_id.unwrap_or("Default")
                );
                print!(
                    "artive time {}",
                    stop_time_update
                        .arrival
                        .and_then(|arrival| arrival.time)
                        .unwrap_or("Default")
                );
                print!(
                    "departure time {}",
                    stop_time_update
                        .departure
                        .and_then(|departure| departure.time)
                        .unwrap_or("Default")
                );
            }
        }
    }

This is starting to get a little unruly because we can't use the ? on the nested options because the return type is Result. Yeah we could make another function, but that feels like a work around. Also, this isn't even that deep down, I've seen some GTFS schemas get very complex. Ideally I would want something like this:


for entity in feed_entity {
  for stop_time_update in (entity.trip_update?.stop_time_update ?? []) {
    print!("stop id: {} ", stop_time_update.stop_id.unwrap_or("Default"));
    print!("arrival time: {} ", stop_time_update.arrival?.time ?? "Default");
    print!("departure time: {} ", stop_time_update.departure?.stop_id ?? "Default");
  }
}

Ok so what's happening here. I've kindof seperated the use case of ?? from unwrap_or as you can see everything on the LHS of a ?? has a ? in it. Basically what I want is:

entity.trip_update?.stop_time_update ?? []

to de-sugar to

(|entity: FeedEntity| entity.trip_update?.stop_time_update)(entity)
            .unwrap_or([]);

So now we are able to use the ? on an option to get a nested option without having to make another function because of the closure. Obviously actually typing out this closure is not any better than the original, but with the syntactic sugar it could be really nice. Also, I think ?? would still be able to be used in all of the place unwrap_or. I think this is the much more important use case that actually makes things a lot easier to read and write.

2 Likes

No longer think it needs to be an operator. But being able to do this general functionality with some syntactic sugar. Thinking like nested_options!(deeply_nested_option, default value)

If you're desugaring to an IIFE to bound the scope of ?, you're looking for try_blocks - The Rust Unstable Book

2 Likes

I don't think we should add this as an operator, but I can imagine adding this as an extension to let-else. There was some discussion about that in the let-else RFC, though we deferred that to the future in favor of shipping a simpler let-else.

2 Likes

In light of this post on the horrible debug performance of C++, perhaps we should support something equiavalent to the ?: operator proposed above. Maybe it could be a postfix macro, or some new operator, or some generalization of an existing feature, but we need some way to not trash the performance of debug builds while writing idiomatic Rust.