Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditionally return empty iterator from flat_map

Given this definition for foo:

let foo = vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]];

I'd like to be able to write code like this:

let result: Vec<_> = foo.iter()
    .enumerate()
    .flat_map(|(i, row)| if i % 2 == 0 {
        row.iter().map(|x| x * 2)
    } else {
        std::iter::empty()
    })
    .collect();

but that raises an error about the if and else clauses having incompatible types. I tried removing the map temporarily and I tried defining an empty vector outside the closure and returning an iterator over that like so:

let empty = vec![];

let result: Vec<_> = foo.iter()
    .enumerate()
    .flat_map(|(i, row)| if i % 2 == 0 {
        row.iter() //.map(|x| x * 2)
    } else {
        empty.iter()
    })
    .collect();

This seems kind of silly but it compiles. If I try to uncomment the map then it still complains about the if and else clauses having incompatible types. Here's part of the error message:

error[E0308]: if and else have incompatible types
  --> src/main.rs:6:30
   |
6  |           .flat_map(|(i, row)| if i % 2 == 0 {
   |  ______________________________^
7  | |             row.iter().map(|x| x * 2)
8  | |         } else {
9  | |             std::iter::empty()
10 | |         })
   | |_________^ expected struct `std::iter::Map`, found struct `std::iter::Empty`
   |
   = note: expected type `std::iter::Map<std::slice::Iter<'_, {integer}>, [closure@src/main.rs:7:28: 7:37]>`
              found type `std::iter::Empty<_>`

Playground Link

I know I could write something that does what I want with some nested for loops but I'd like to know if there's a terse way to write it using iterators.

like image 255
Ryan1729 Avatar asked Aug 20 '17 21:08

Ryan1729


2 Answers

Since Rust is statically typed and each step in an iterator chain changes the result to a new type that entrains the previous types (unless you use boxed trait objects) you will have to write it in a way where both branches are covered by the same types.

One way to convey conditional emptiness with a single type is the TakeWhile iterator implementation.

.flat_map(|(i, row)| {
    let iter = row.iter().map(|x| x * 2);
    let take = i % 2 == 0;
    iter.take_while(|_| take)
})

If you don't mind ignoring the edge-case where the input iterator foo could have more than usize elements you could also use Take instead with either 0 or usize::MAX. It has the advantage of providing a better size_hint() than TakeWhile.

like image 124
the8472 Avatar answered Sep 24 '22 03:09

the8472


In your specific example, you can use filter to remove unwanted elements prior to calling flat_map:

let result: Vec<_> = foo.iter()
    .enumerate()
    .filter(|&(i, _)| i % 2 == 0)
    .flat_map(|(_, row)| row.iter().map(|x| x * 2))
    .collect();

If you ever want to use it with map instead of flat_map, you can combine the calls to filter and map by using filter_map which takes a function returning an Option and only keeps elements that are Some(thing).

like image 21
Jmb Avatar answered Sep 20 '22 03:09

Jmb