Search code examples
rustrust-result

Calling map on Iter of Results in Rust


I would like to write some code in a "functional programming" style.

However, I start with an Iterator of Results and I only want to apply the function to the Ok items. Furthermore, I want to stop the iteration on the first error (however, I'd be open to different behavior).

So far, I am using a nested map() pattern: <iter>.map(|l| l.map(replace)). I think this is extremely ugly.

Using the nightly "result_flattening", I can flatten each nested Result<Result<T, E>, E> into a Result<T, E>. Using eyre::Context I convert the different Error types into an eyre::Report error type. All of this feels quite clumsy.

What is an elegant way to write this in Rust?

Minimal Working Example

#![feature(result_flattening)]
use std::io::BufRead;

use eyre::Context;

fn main() {
    let data = std::io::Cursor::new(b"FFBFFFBLLL\nBFBFBBFRLR\nFFFBFFBLLL");

    let seats: Result<Vec<_>, _> = data
        .lines()
        .map(|l| l.map(replace).context("force eyre"))
        .map(|l| l.map(|s| u32::from_str_radix(&s, 2).context("force eyre")))
        .map(|r| r.flatten())
        .collect();

    println!("{:#?}", seats);
}

fn replace(line: String) -> String {
    line.replace('F', "0")
        .replace('B', "1")
        .replace('L', "0")
        .replace('R', "1")
}

Further References:


Solution

  • Since you discard the error type anyway, you can avoid eyre entirely and use .ok to convert the Result into an Option, then just work with Option's and_then to avoid flattening every time:

    let seats: Option<Vec<_>> = data
        .lines()
        .map(|l| l.ok())
        .map(|l| l.map(replace))
        .map(|l| l.and_then(|s| u32::from_str_radix(&s, 2).ok()))
        // if you want to keep chaining
        .map(|l| l.and_then(|s| some_result_function(&s).ok()))
        .collect();
    

    If you want to just skip over the errors, a much more elegant solution exists with filter_map:

    let seats: Vec<_> = data
        .lines()
        .filter_map(|l| l.ok())
        .map(replace)
        .filter_map(|l| u32::from_str_radix(&l, 2).ok())
        .collect();
    

    If you want to maintain errors, then box the errors into a Box<dyn Error> to account for different types:

    use std::error::Error;
    // later in the code
    let seats: Result<Vec<_>, Box<dyn Error>> = data
        .lines()
        .map(|x| x.map_err(|e| Box::new(e) as _))
        .map(|l| l.map(replace))
        .map(|l| l.and_then(|s| u32::from_str_radix(&s, 2).map_err(|e| Box::new(e) as _)))
        .collect();
    

    If you don't like the repeated .map_err(|e| Box::new(e) as _), then make a trait for it:

    use std::error::Error;
    
    trait BoxErr {
        type Boxed;
        fn box_err(self) -> Self::Boxed;
    }
    
    impl<T, E: Error + 'static> BoxErr for Result<T, E> {
        type Boxed = Result<T, Box<dyn Error>>;
        
        fn box_err(self) -> Self::Boxed {
            self.map_err(|x| Box::new(x) as Box<dyn Error>)
        }
    }
    
    // later in the code
    
    let seats: Result<Vec<_>, Box<dyn Error>> = data
        .lines()
        .map(|x| x.box_err())
        .map(|l| l.map(replace))
        .map(|l| l.and_then(|s| u32::from_str_radix(&s, 2).box_err()))
        .collect();