4

I have a function that returns Result<Option<[type]>> on an Input type:

pub struct Input {
    a: Option<i32>,
    b: Option<i32>,
    c: Option<i32>,
}

For each field in input if the value is None, the function will immediately return Ok(None) and if there's a value, the function will unwrap it and proceed with further logic:

fn process(input: Input) -> std::io::Result<Option<i32>> {
    let a = match input.a {
        Some(a) => a,
        None => return Ok(None),
    };
    // the same for b and c...
    todo!()
}

The same pattern in repeated for all the fields in the input type. Is there a prettier way to express this?

Elias Holzmann
  • 3,216
  • 2
  • 17
  • 33
Nasif Imtiaz Ohi
  • 1,563
  • 5
  • 24
  • 45
  • 1
    I needed something similar when iterating over files in a directory, where I'd use `?` to handle errors (by stopping the iteration that collects into `Result>`) but would have also likeed to use `?` on options to skip some files (without stopping the iteration, e.g. using `filter_map`). Unfortunately I don't think you can convince `?` to return `Ok(...)`. – user4815162342 Jul 06 '21 at 16:10
  • [What's the idiomatic way to handle multiple `Option` in Rust?](https://stackoverflow.com/q/50731439/155423) – Shepmaster Jul 06 '21 at 17:36

3 Answers3

7

You could use a if let to assign all variables at once and return Ok(None) in the else branch:

fn process(input: Input) -> std::io::Result<Option<i32>> {
    if let (Some(a), Some(b), Some(c)) = (input.a, input.b, input.c) {
        todo!();
    } else {
        return Ok(None)
    }
}

Playground link

Edit: Or, even more concise (thanks to Dietrich Epp for the suggestion!):

fn process(input: Input) -> std::io::Result<Option<i32>> {
    if let Input {
        a: Some(a),
        b: Some(b),
        c: Some(c),
    } = input
    {
        todo!();
    } else {
        return Ok(None);
    }
}

Playground link

Elias Holzmann
  • 3,216
  • 2
  • 17
  • 33
  • Is there way to have some output generated within `if let` block and use it afterwards within the function, or I am constrained to move my whole function logic within the `if let` block? – Nasif Imtiaz Ohi Jul 06 '21 at 16:22
  • 1
    @NasifImtiazOhi `if let` is (as most control flow structures in Rust) an expression. If you end the `if let` body with an expression, this is also the result of the `if let` expression. An [example](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=60bb4b5be8a1cbcf03c3144db74deab7) will probably make this more clear. – Elias Holzmann Jul 06 '21 at 16:28
  • 1
    @NasifImtiazOhi You can transform original match to use a tuple in the same way: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=05b4ba388d745eeae1d93f4427350a19 – user4815162342 Jul 06 '21 at 16:29
  • (Of course, this won't work if your options aren't neatly available in a struct, but you need to calculate them with code in-between, but for your case it should work nicely.) – user4815162342 Jul 06 '21 at 16:31
4

On nightly, you can use try_blocks:

#![feature(try_blocks)]

let x: Option<_> = try { (input.a?, input.b?, input.c?) };

let (a, b, c) = match x {
    Some((a, b, c)) => (a, b, c),
    None => return Ok(None),
};

A stable alternative is creating and immediately calling a closure, although it doesn't look as nice:

let x = (|| { Some((input.a?, input.b?, input.c?)) })();

let (a, b, c) = match x {
    Some((a, b, c)) => (a, b, c),
    None => return Ok(None),
};

Maybe the cleanest solution is Option::and_then combined with the ? operator:

let x = input.a.and_then(|a| Some((a, input.b?, input.c?)));

let (a, b, c) = match x {
    Some((a, b, c)) => (a, b, c),
    None => return Ok(None),
};

You can also use Option::zip, although you'll end up with nested tuples:

let x = input.a.zip(input.b).zip(input.c);

let (a, b, c) = match x {
    Some(((a, b), c)) => (a, b, c),
    None => return Ok(None),
};
Ibraheem Ahmed
  • 11,652
  • 2
  • 48
  • 54
1

Another possibility (but @sk_pleasant's version is way cleaner). Playground

fn main() {
    let inp = Input { a:Some(1), b:Some(2), c:Some(3) };
    let unwrapped = 
        inp.a.map_or(None, |a| 
            inp.b.map_or(None, |b| 
                inp.c.map_or(None, |c| Some((a, b, c)))));   
    match unwrapped {
        Some((a, b, c)) => println!("{} {} {}", a, b, c),
        None => println!("None"), // return Ok(None);
    }
}
Alexey S. Larionov
  • 6,555
  • 1
  • 18
  • 37