2

I created this example code:

fn main() {
    let books = vec![
        Book {
            data: Ok("type1".to_owned()),
            metadata: "meta1".to_owned(),
        },
        Book {
            data: Err("-".to_owned()),
            metadata: "meta2".to_owned(),
        },
        Book {
            data: Ok("type2".to_owned()),
            metadata: "meta2".to_owned(),
        },
    ];

    // metadata without data being error
    let (book_type_1, book_type_2): &(Vec<_>, Vec<_>) = &books
        .iter()
        .filter(|f| f.data.is_ok())
        .partition(|p| p.data.as_ref().unwrap() == "type1");

    println!("Books {:?}", books);
    println!("Type 1 {:?}", book_type_1); // Should be the original Vec<Book> with Type 1 filtered.
    println!("Type 2 {:?}", book_type_2); // Should be the original Vec<Book> with Type 2 filtered.
}

#[derive(Debug)]
struct Book {
    data: Result<String, String>,
    metadata: String,
}

On the let (book_type_1, book_type_2) expression, I need to use Book::data twice, but I already filtered it so I know it can't be Err. Is there a way to restructure to remove the use of unwrap here?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Athiwat Chunlakhan
  • 7,589
  • 14
  • 44
  • 72

2 Answers2

3

I would use flat_map as described in the other answer, but I'd leave the yielded values as a Result. Result implements IntoIterator, so you can use it directly in flat_map.

I'd also use Result::as_ref instead of writing out the match explicitly.

I'd then use Itertools::partition_map to simultaneously select between the types and remove the extra property:

extern crate itertools;
use itertools::{Either, Itertools};

// ...

let (book_type_1, book_type_2): (Vec<_>, Vec<_>) = books
    .iter()
    .flat_map(|b| b.data.as_ref().map(|data| (b, data)))
    .partition_map(|(b, data)| {
        if data == "type1" {
            Either::Left(b)
        } else {
            Either::Right(b)
        }
    });

Note:

  • There's no reason to take a reference to the result tuple.
  • This only works because you are iterating on references to books.

If you needed to operate on the owned Books, I'd move the comparison into the flat_map call and pass it along:

let (book_type_1, book_type_2): (Vec<_>, Vec<_>) = books
    .into_iter()
    .flat_map(|book| {
        book.data
            .as_ref()
            .ok()
            .map(|data| data == "type1")
            .map(|is_type1| (is_type1, book))
    })
    .partition_map(|(is_type1, book)| {
        if is_type1 {
            Either::Left(book)
        } else {
            Either::Right(book)
        }
    });

There's also the pragmatic solution of sorting all errors into the same bin as one of the types. Since you know that there won't be any errors, this has no effect:

let (book_type_1, book_type_2): (Vec<_>, Vec<_>) = books
    .iter()
    .filter(|f| f.data.is_ok())
    .partition(|p| p.data.as_ref().ok().map_or(false, |d| d == "type1"));

See also:

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
2

I'm not sure exactly what you mean, but it seems like you want to use Iterator::filter_map(). It lets you filter for values that are Some(T), which then get passed on as unwrapped Ts.

So what you can do is convert your Results to Options with Result::ok(), so a Result::Ok(T) will become Some(T) which means it passes the filter as T.

fn main() {
    let books = vec![
        Book {
            data: Ok("type1".to_owned()),
            metadata: "meta1".to_owned(),
        },
        Book {
            data: Err("-".to_owned()),
            metadata: "meta2".to_owned(),
        },
        Book {
            data: Ok("type2".to_owned()),
            metadata: "meta2".to_owned(),
        },
    ];

    // metadata without data being error
    let (book_type_1, book_type_2): (Vec<_>, Vec<_>) = books
        .iter()
        .filter_map(|f| {
            match &f.data {
                Ok(data) => Some((f, data)),
                Err(_) => None,
            }
        })
        .partition(|(book, data)| *data == "type1");

    println!("Books {:?}", books);
    println!("Type 1 {:?}", book_type_1);
    println!("Type 2 {:?}", book_type_2);
}

#[derive(Debug)]
struct Book {
    data: Result<String, String>,
    metadata: String,
}

playground

I removed the unnecessary reference to the returned partitioned tuple.

Also note that None pertains to Option<T>, but you're using Result<T, E>. I think you knew that but just making sure.

Jorge Israel Peña
  • 36,800
  • 16
  • 93
  • 123
  • Sorry if I wasn't clear. As for your example. The `metadata` is now missing, what I want is the whole `Book`. (I kept using `None` for `Err`, my mistake I will fix it) – Athiwat Chunlakhan Jun 09 '18 at 08:50
  • Ah I see what you mean. In that case you can't unwrap the `data` field in-place since the `data` field is specifically a `Result`. One thing you can do is unwrap it and store a copy or reference alongside `Book` in some wrapper type that would contain both values, such as a tuple. I'll write an example. Honestly, I get where you're coming from, but at that point it would be more work and more complexity than it's worth compared to just keeping the unwrap. – Jorge Israel Peña Jun 09 '18 at 09:00
  • I had an example with `fold` working, where I just accumulate it as a tuple and discard the one where the `Result` is `Err`, but like you said it gets very long and compare to just `unwrap`. – Athiwat Chunlakhan Jun 09 '18 at 09:03
  • Just in case anyone need to turn it back to just a Book. [Playground](https://play.rust-lang.org/?gist=60ff77b80e9bc519c7856aaf71b5acad&version=stable&mode=debug) – Athiwat Chunlakhan Jun 09 '18 at 09:21