Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to group 'Option' assignments in Rust?

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);
            }
        }
    }
}
like image 798
ideasman42 Avatar asked Dec 06 '16 01:12

ideasman42


3 Answers

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

like image 134
Erik Vesteraas Avatar answered Nov 13 '22 14:11

Erik Vesteraas


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

like image 5
SplittyDev Avatar answered Nov 13 '22 16:11

SplittyDev


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"),
    }
}
like image 5
Shepmaster Avatar answered Nov 13 '22 15:11

Shepmaster