22

I'm wading through a codebase full of code like this:

if let Some(i) = func1() {
    if let Some(j) = func2(i) {
        if let Some(k) = func3(j) {
            if let Some(result) = func4(k) {
                // Do something with result
            } else {
                println!("func 4 returned None");
            }
        } else {
            println!("func 3 returned None");
        }
    } else {
        println!("func 2 returned None");
    }
} else {
    println!("func 1 returned None");
}

That's a stupid, simplified example, but the general pattern is that:

  • There are a bunch of different functions which return an Option.
  • All the functions must be called in sequence, with the return value of the previous function (if it's not None) passed to the next function.
  • If all functions return a Some, then the final returned is used for something.
  • If any returns None, then execution stops and some kind of error is logged - usually an error message that informs the user exactly which function returned None.

The problem, of course, is that the above code is an ugly and unreadable. It gets even uglier when you substitute i, func1 etc. with variable/function names that actually mean something in the real code, and many examples in my real codebase have far more than four nested if lets. It's an example of the arrow anti-pattern, it completely fails the squint test, and it's confusing how the error messages appear in reverse order to the functions which can cause them.

Is there really not a better way to do this? I want to refactor the above into something that has a cleaner, flatter structure where everything appears in a sensible order. if let chaining might help but it doesn't look like that feature is available in Rust yet. I thought maybe I could clean things up by using ? and/or extracting some helper functions, but I couldn't get it to work and I'd rather not extract a ton of new functions all over the place if I can avoid it.

Here's the best I could come up with:

let i : u64;
let j : u64;
let k : u64;
let result : u64;

if let Some(_i) = func1() {
    i = _i;
} else {
   println!("func 1 returned None");
   return;
}
if let Some(_j) = func2(i) {
    j = _j;
} else {
   println!("func 2 returned None");
   return;
}
if let Some(_k) = func3(j) {
    k = _k;
} else {
   println!("func 3 returned None");
   return;
}
if let Some(_result) = func3(k) {
    result = _result;
} else {
   println!("func 4 returned None");
   return;
}


// Do something with result

But this still feels very long and verbose, and I don't like how I'm introducing these extra variables _i, _j etc.

Is there something I'm not seeing here? What's the simplest and cleanest way to write what I want to write?

Herohtar
  • 5,347
  • 4
  • 31
  • 41
GMA
  • 5,816
  • 6
  • 51
  • 80

5 Answers5

22

You can use let-else statements, a feature which was added to stable rust in version 1.65.

RFC 3137

Introduce a new let PATTERN: TYPE = EXPRESSION else DIVERGING_BLOCK; construct (informally called a let-else statement), the counterpart of if-let expressions.

If the pattern match from the assigned expression succeeds, its bindings are introduced into the surrounding scope. If it does not succeed, it must diverge (return !, e.g. return or break).

With this feature you can write:

let Some(i) = func1() else {
    println!("func 1 returned None");
    return;
};
let Some(j) = func2(i) else {
    println!("func 2 returned None");
    return;
};
let Some(k) = func3(j) else {
    println!("func 3 returned None");
    return;
};
let Some(result) = func3(k) else {
    println!("func 4 returned None");
    return;
};

If you wanted to try it on an older version of rust, you would have to enable the unstable feature:

#![feature(let_else)]
sourcejedi
  • 3,051
  • 2
  • 24
  • 42
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
  • 2
    FWIW there's a [crate which does that as a macro](https://crates.io/crates/guard). The syntax is basically the same, except enclosed in a `guard!()`, and it needs a final semicolon. – Masklinn Feb 25 '22 at 14:40
  • 5
    This is now stable as of 1.65 – cameron1024 Nov 07 '22 at 11:39
15

If-let chaining will make this a lot nicer, but for now (assuming you don't want to use nightly), it's possible a slight refactor could help. For example, pulling all but the last call of the chain into its own function allows you to use the ? operator:

fn get_result() -> Option<u64> {
  let i = func1()?;
  let j = func2(i)?;
  let k = func3(j)?;
  func3(k)
}

fn main() {
  if let Some(result) = get_result() {
    // do something
  }
}

If you need more fine-grained control over the error cases, you could return a Result instead:

enum Error {
  Func1,
  Func2,
  Func3,
  Func4,
}

fn get_result() -> Result<i64, Error> {
  let i = func1().ok_or(Error::Func1)?;
  let j = func2(i).ok_or(Error::Func2)?;
  let k = func3(j).ok_or(Error::Func3)?;
  func4(k).ok_or(Error::Func4)
}

fn main() {
  use Error::*;
  match get_result() {
    Ok(result) => {},
    Err(Func1) => {},
    // ...
  }
}
cameron1024
  • 9,083
  • 2
  • 16
  • 36
7

I'd like to put two more things on your list of "eventually look into" things: The simple Option::and_then:

let result = func1().and_then(func2).and_then(func3).and_then(func4);
match result {
  Some(result) => …,
  None => …,
}

And the slightly more tricky, but incredibly convenient anyhow::Context:

use anyhow::Context;
let j = func2(i).context("Func2 failed")?;
Caesar
  • 6,733
  • 4
  • 38
  • 44
4

A slightly better version of the if let ... else { return } can be used:

let i = if let Some(i) = func1() { i } else {
   println!("func 1 returned None");
   return;
};
let j = if let Some(j) = func2(i) { j } else {
   println!("func 2 returned None");
   return;
};
let k = if let Some(k) = func3(j) { k } else {
   println!("func 3 returned None");
   return;
};
let result = if let Some(result) = func3(k) { result } else {
   println!("func 4 returned None");
   return;
};
Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
  • Thanks. Is there any way I can achieve this without needing to extract a new function? I can see how extracting a new function will often make things cleaner, but sometimes it would be easier if I don't have to, especially if there aren't many `if`s required. Do I have a choice? – GMA Feb 27 '22 at 11:38
  • @GMA Not really, unfortunately. – Chayim Friedman Feb 27 '22 at 23:58
1

I had a very similar issue and the solution of @Caesar helped me a lot.

I would add in my case I need the results of all the functions involved, and I've modified that code to achieve that in this way:

let Some((first, Some(second)) = a.func().and_then(|first| Some((first, b.func(second))) {
    result += c.func(first, second);
}

The down side is it could end up by having a lot of nested Some for long chains, but in my case I only have two levels and this solution seems to be quite efficient.

PS: cargo clippy suggested to use map instead of and_then, which is indeed shorter and cleaner in this case :)

mikysett
  • 11
  • 2