How to group 'Option' assignments in Rust?
Asked Answered
D

5

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);
            }
        }
    }
}
Dowager answered 6/12, 2016 at 1:6 Comment(4)
I was literally about to put this as an answer 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.Lunula
@SimonWhitehead, thanks all the same, added it to the question for clarification - since it may be handy in some situations still.Dowager
Duplicate #53235977Merriweather
Does this answer your question? Does Rust 2018 support "if let" chaining?Merriweather
C
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

Clippers answered 6/12, 2016 at 2:59 Comment(4)
Really nice, minor wart is it requires the body to be a macro argument, which makes me prefer @SplittyDev's answer.Dowager
@Dowager 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.Clippers
@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.Dowager
@Dowager 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.Clippers
G
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

Gracie answered 6/12, 2016 at 1:54 Comment(2)
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!).Lunula
@SimonWhitehead honestly, I'm quite new to macros too. You should've seen my face when I realized this actually works.Gracie
B
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"),
    }
}
Brigettebrigg answered 6/12, 2016 at 2:45 Comment(1)
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!Lunula
N
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"),
}
Neighbor answered 6/12, 2016 at 2:32 Comment(1)
Works, but gets progressively worse with more arguments.Gracie
C
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);
    }
}
Cellulose answered 1/2, 2021 at 3:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.