Nested "Match hell" in Rust?

Hi... I'm pretty new to Rust and its pattern. Recently I noticed something inconvenient in my code.

I have 3 functions or more that output Result<T,E> or Option<T>. These functions need to be executed in series.

fn do_something () -> Option<i32> {
    
}

fn summarize_something (a: i32) -> Option<i32> {
    
}

fn process_something (b: i32) -> Option<i32> {
    
}

I also need to catch any error that might be generated by these functions. So here's my implementation below. This is reminiscent of the "callback hell" in javascript. It's hard to read and confusing to debug when the code is long.

 fn main(){
    let result = do_something()
    if let Some(i) = result {
        let summary = summarize_something(i)
        if let Some(j) = summary {
            let processed = process_something(j)
            if let Some(k) = processed {
                // .... and etc. This will be repeated for quite some time
            } else {
                 println!("Error happened in process_something()");
            }
        } else {
            println!("Error happened in summarize_something()");
        }
    } else {
        println!("Error happened in do_something()");
    }
}

How to refactor this complex code ?. Am I missed something ? Or it is what it is ... ?

Thank you in advance

1 Like

You're probably looking for something like if_chain.

5 Likes

Would you mind to give me an example for 3 or more "ifs" ?
Also how to catch the error with if_chain! ?

if_chain! {
    if let Some(y) = x;
    if let Some(z) = y;
    then {
        do_stuff_with(z)
    } else {
        do_something_else()
    }
}
1 Like

You could refactor to handle the errors as values. Error handling in Rust is a large topic, but as a very brief introduction to illustrate how it can reduce the nesting and other boilerplate:

pub enum SomeError {
    Doing,
    Summarizing,
    Processing,
}

// The `foo = x?` below do something similar to:
//    foo = match x {
//        Ok(foo) => foo,
//        Err(e) => return Err(e),
//    };
// So errors get returned out of the entire function,
// while non-errors get assigned and the function continues
fn do_something() -> Result<(), SomeError> {
    // do_thing returns e.g. Result<_, SomeError>
    let result = do_thing()?; 
    // summarize_something returns Option<_>
    // The .ok_or() turns it into a Result<_, SomeError>
    let summary = summarize_something(i).ok_or(SomeError::Summarizing)?;
    // process_something returns Result<_, SomeOtherErrorType>
    // The .map_err() turns it into a Result<_, SomeError>
    let processed = process_something(summary).map_err(|e| SomeError::Processing)?;
}

fn main() {
    match do_something() {
        Ok(_) => {},
        Err(e) => handle_your_error_somehow(e),
    }
}
11 Likes

Here's a similar version that shows one way to mix Option and Result and str types:

fn do_stuff() -> Result<(), &'static str> {
    let result = do_something().ok_or("Error happened in do_something()")?;
    let summary = summarize_something(result).ok_or("Error happened in summarize_something()")?;
    let processed = process_something(summary).ok_or("Error happened in process_something()")?;
    // .... and etc. This will be repeated for quite some time
    Ok(())
}

fn main() {
    if let Err(e) = do_stuff() {
        println!("{}", e);
    }
}

(Playground)

12 Likes

Another slight variation uses a control flow operator unpack which generalizes unwrap_or_else.

macro_rules! unpack {
    ($x:expr, $variant:path, $otherwise:expr) => {
        match $x {
            $variant(value) => value,
            _ => $otherwise
        }
    }
}

fn do_stuff() -> Result<(), &'static str> {
    let i = unpack!(do_something(), Some,
        return Err("Error happened in do_something()")
    );
    let j = unpack!(summarize_something(i), Some,
        return Err("Error happened in summarize_something()")
    );
    let _k = unpack!(process_something(j), Some,
        return Err("Error happened in process_something()")
    );
    // ....
    Ok(())
}

fn main() {
    if let Err(e) = do_stuff() {
        println!("{}", e);
    }
}

I was also playing with the calling map on Option for each function call. I could not find a way without nesting... but is there?

Yes, but you instead of map you want and_then (aka flatmap, aka monadic bind)

2 Likes

... where there is no way to know which function returned None so not what is desired here. Got it. Thanks.

Not with Option, no. You need to convert everything to Result, like mbrubeck did, to keep track of which one failed. If you don't want to use the ? operator, you can do the job using Result::and_then

    let processed = do_something().ok_or("Error happened in do_something()")
        .and_then(|i| summarize_something(i).ok_or("Error happened in summarize_something()"))
        .and_then(|j| process_something(j).ok_or("Error happened in process_something()"));
6 Likes