Question mark operator in function that returns unit?

In a function body that returns Option<T>, I can use the ? operator either get the Some or early-return None.

fn foo(thing: Thing) -> Option<i32> {
   let x = thing.foo()?.bar()?;
   ...
}

I have some code that defines an event handler function like this one below. The handler closure returns nothing (ie, ()). My typical pattern is to unwrap a few Options.

button.on_click(move |_, c| {
    let Some(controller) = c.upgrade() else { return };
    let Some(nav) = controller.find_navigator() else { return };
    nav.pop();
}

Is there a convenient way to turn that into something like this below?

button.on_click(move |_, c| {
    c.upgrade()?.find_navigator()?.pop()
}

Monadic style:

c.upgrade()
    .and_then(|con| con.find_navigator())
    .and_then(|nav| nav.pop());

Also try blocks, but unfortunately they're still unstable. The best stable replacement is probably an immediately invoked lambda:

(|| {
    c.upgrade()?.find_navigator()?.pop();
})();
10 Likes

Cool, it’s nice to see the improvements in the pipeline.

For my code, that immediately invoked lambda doesn’t type check. It takes it to be a ()-returning function again.

Yeah, you may have to fiddle with it a bit, if the last call in the chain doesn't return an Option anymore, you have to add a dummy Some(()) as the lambda's return value to make it typecheck.

Note that for the easy cases like this you can probably use a chain instead, like

button.on_click(move |_, c| {
    if let Some(controller) = c.upgrade()
        && Some(nav) = controller.find_navigator() 
    {
        nav.pop();
    }
}

But yeah, long-term the answer will be try blocks. (I've been working on those recently to try to get them to stable.)

9 Likes

Would this stabilization include FromResidual? (I know you can't make any promises, but would stabilization of try blocks automatically include stabilization of FromResidual?).

Another solution is to just define a regular function:

fn handle_click(c: ...) -> Option<()> {
    c.upgrade()?.find_navigator()?.pop();
    Some(())
}
    
button.on_click(move |_, c| { handle_click(c); });
2 Likes

The stabilization of the traits is probably separate from the stabilization of try blocks themselves, like how ? is stable without the traits being stable.

1 Like

Often overlooked, the function could be defined in the closure itself:

button.on_click(move |_, c| {
    fn handle_click(c: ...) -> Option<()> {
        c.upgrade()?.find_navigator()?.pop();
        Some(())
    }
    handle_click(c);
}

This keeps locality, and you can be lazy with function names: fn inner(...)... :wink:

2 Likes

You can use this trick:

macro_rules! try_block_at_home {
    ($($e:tt)*) => {
        (|| std::iter::empty::<std::convert::Infallible>().try_fold({$($e)*}, |_, x| match x {}))()
    }
}

let _: Option<i32> = try_block_at_home! {
    Some(3)?;
    1
};
let _: Option<()> = try_block_at_home! {
    Some(3)?;
};
let _: std::io::Result<_> = try_block_at_home! {
    let v = std::fs::read("asdf.txt")?;
    v.len()
};

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1400a851e11bf4676df3bf4a3f65cc3a

(Though that's not quite how I currently expect try blocks will work.)

I often use Option<Infallible>, this is equivalent to (). Soon Option<!> should be possible, if not already.

6+ months probably.

1 Like