9

I have a block of code where multiple optional variables need to be assigned at once. There is very little chance any of the values will be None, so individually handing each failed case isn't especially useful.

Currently I write the checks like this:

if let Some(a) = foo_a() {
    if let Some(b) = foo_b() {
        if let Some(c) = foo_c() {
            if let Some(d) = foo_d() {
                // code
            }
        }
    }
}

It would be convenient if it was possible to group assignments. Without this, adding a new variable indents the block one level, making for noisy diffs and causes unnecessarily deep indentation:

if let Some(a) = foo_a() &&
   let Some(b) = foo_b() &&
   let Some(c) = foo_c() &&
   let Some(d) = foo_d()
{
    // code
}

Is there a way to assign multiple Options in one if statement?


Some details worth noting:

The first function that fails should short circuit and not call the others. Otherwise, it could be written like this:

if let (Some(a), Some(b), Some(c), Some(d)) = (foo_a(), foo_b(), foo_c(), foo_d()) {
    // Code
}

Deep indentation could be avoided using a function, but I would prefer not to do this since you may not want to have the body in a different scope...

fn my_function(a: Foo, b: Foo, c: Foo, d: Foo) {
    // code
}

if let Some(a) = foo_a() {
    if let Some(b) = foo_b() {
        if let Some(c) = foo_c() {
            if let Some(d) = foo_d() {
                my_function(a, b, c, d);
            }
        }
    }
}
ideasman42
  • 42,413
  • 44
  • 197
  • 320
  • 2
    I was literally about to [put this as an answer](https://play.rust-lang.org/?gist=19b24cb31e915860916a99f41347b727&version=stable&backtrace=0) until I noticed your edit that included short-circuiting. I don't think its possible to short-circuit multiple `if let` bindings. There is [however an open RFC for it](https://github.com/rust-lang/rfcs/issues/929). – Simon Whitehead Dec 06 '16 at 01:21
  • @SimonWhitehead, thanks all the same, added it to the question for clarification - since it may be handy in some situations still. – ideasman42 Dec 06 '16 at 01:25
  • Duplicate https://stackoverflow.com/questions/53235477/does-rust-2018-support-if-let-chaining/53237008 – Chayim Friedman Sep 17 '20 at 19:58
  • Does this answer your question? [Does Rust 2018 support "if let" chaining?](https://stackoverflow.com/questions/53235477/does-rust-2018-support-if-let-chaining) – Chayim Friedman Sep 17 '20 at 19:59

5 Answers5

10

As @SplittyDev said, you can create a macro to get the functionality you want. Here is an alternate macro-based solution which also retains the short-circuiting behaviour:

macro_rules! iflet {
    ([$p:pat = $e:expr] $($rest:tt)*) => {
        if let $p = $e {
            iflet!($($rest)*);
        }
    };
    ($b:block) => {
        $b
    };
}


fn main() {
    iflet!([Some(a) = foo_a()] [Some(b) = foo_b()] [Some(c) = foo_c()] {
        println!("{} {} {}", a, b, c);
    });
}

Playground

Community
  • 1
  • 1
Erik Vesteraas
  • 4,675
  • 2
  • 24
  • 37
  • Really nice, minor wart is it requires the `body` to be a macro argument, which makes me prefer @SplittyDev's answer. – ideasman42 Dec 09 '16 at 23:38
  • @ideasman42 Macro argument? As far as I understand the body can be any arbitrary code block. I'll admit I'm not that well versed in the restrictions on macros, so if you have a good example of something that would not work with this macro it would certainly be a good amendment to the answer for future visitors. – Erik Vesteraas Dec 10 '16 at 01:10
  • @Eric, theres no error in the macro - as you say it can take any block of code as an argument, it just reads a little awkward having `if macro!(args, { body });` compared to `if macro!(args) { body }` its not that its especially bad, just my personal preference to avoid it - given the choice, and assuming the alternative isn't worse in some other way. – ideasman42 Dec 10 '16 at 01:40
  • @ideasman42 Yeah, I get what you're saying and I have to agree. It's not quite as good as native syntax, so it comes down to personal preference in the end. You could always fiddle with the exact syntax for the macro arguments but you will always need something to disambiguate arguments from body. – Erik Vesteraas Dec 10 '16 at 01:50
5

The standard library doesn't include that exact functionality, but the language allows you to create the desired behavior using a small macro.

Here's what I came up with:

macro_rules! all_or_nothing {
    ($($opt:expr),*) => {{
        if false $(|| $opt.is_none())* {
            None
        } else {
            Some(($($opt.unwrap(),)*))
        }
    }};
}

You can feed it all your options and get some tuple containing the unwrapped values if all values are Some, or None in the case that any of the options are None.

The following is a brief example on how to use it:

fn main() {
    let foo = Some(0);
    let bar = Some(1);
    let baz = Some(2);
    if let Some((a, b, c)) = all_or_nothing!(foo, bar, baz) {
        println!("foo: {}; bar: {}; baz: {}", a, b, c);
    } else {
        panic!("Something was `None`!");
    }
}

Here's a full test-suite for the macro: Rust Playground

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
SplittyDev
  • 551
  • 1
  • 10
  • 19
  • 1
    Great answer! I considered the macro approach but couldn't quite figure out how to go about implementing it (macros are still a bit intimidating for me!). – Simon Whitehead Dec 06 '16 at 02:05
  • 1
    @SimonWhitehead honestly, I'm quite new to macros too. You should've seen my face when I realized this actually works. – SplittyDev Dec 06 '16 at 02:13
5

My first inclination was to do something similar to swizard's answer, but to wrap it up in a trait to make the chaining cleaner. It's also a bit simpler without the need for extra function invocations.

It does have the downside of increasing the nesting of the tuples.

fn foo_a() -> Option<u8> {
    println!("foo_a() invoked");
    Some(1)
}

fn foo_b() -> Option<u8> {
    println!("foo_b() invoked");
    None
}

fn foo_c() -> Option<u8> {
    println!("foo_c() invoked");
    Some(3)
}

trait Thing<T> {
    fn thing<F, U>(self, f: F) -> Option<(T, U)> where F: FnOnce() -> Option<U>;
}

impl<T> Thing<T> for Option<T> {
    fn thing<F, U>(self, f: F) -> Option<(T, U)>
        where F: FnOnce() -> Option<U>
    {
        self.and_then(|a| f().map(|b| (a, b)))
    }
}

fn main() {
    let x = foo_a()
        .thing(foo_b)
        .thing(foo_c);

    match x {
        Some(((a, b), c)) => println!("matched: a = {}, b = {}, c = {}", a, b, c),
        None => println!("nothing matched"),
    }
}
Community
  • 1
  • 1
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • I really like this to be honest ... and I think I can see where this technique would be applicable in what I am currently working on. Thanks! – Simon Whitehead Dec 06 '16 at 02:50
3

Honestly, someone should notice about Option being an applicative functor :)

The code will be quite ugly without currying support in Rust, but it works and it shouldn't make a noisy diff:

fn foo_a() -> Option<isize> {
    println!("foo_a() invoked");
    Some(1)
}

fn foo_b() -> Option<isize> {
    println!("foo_b() invoked");
    Some(2)
}

fn foo_c() -> Option<isize> {
    println!("foo_c() invoked");
    Some(3)
}

let x = Some(|v| v)
    .and_then(|k| foo_a().map(|v| move |x| k((v, x))))
    .and_then(|k| foo_b().map(|v| move |x| k((v, x))))
    .and_then(|k| foo_c().map(|v| move |x| k((v, x))))
    .map(|k| k(()));

match x {
    Some((a, (b, (c, ())))) =>
        println!("matched: a = {}, b = {}, c = {}", a, b, c),
    None =>
        println!("nothing matched"),
}
swizard
  • 2,551
  • 1
  • 18
  • 26
1

You can group the values using the '?' operator to return an Option of a tuple with the required values. If on of then is None, the group_options function will return None.

fn foo_a() -> Option<u8> {
    println!("foo_a() invoked");
    Some(1)
}

fn foo_b() -> Option<u8> {
    println!("foo_b() invoked");
    None
}

fn foo_c() -> Option<u8> {
    println!("foo_c() invoked");
    Some(3)
}

fn group_options() -> Option<(u8, u8, u8)> {
    let a = foo_a()?;
    let b = foo_b()?;
    let c = foo_c()?;
    Some((a, b, c))
}

fn main() {
    if let Some((a, b, c)) = group_options() {
        println!("{}", a);
        println!("{}", b);
        println!("{}", c);
    }
}
Frank Moreno
  • 304
  • 1
  • 7