10

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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Ryan1729
  • 940
  • 7
  • 25

2 Answers2

7

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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
the8472
  • 40,999
  • 5
  • 70
  • 122
  • "*`|_| if i % 2 == 0 {true} else {false}`*" Please, just `|_| i % 2 == 0`... ;-] – ildjarn Aug 23 '17 at 02:00
  • @ildjarn hah, yes. I also hoisted the modulo out of the closure. although the compiler might be able to optimize it anyway. – the8472 Aug 23 '17 at 06:32
5

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).

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Jmb
  • 18,893
  • 2
  • 28
  • 55