There is a function Iterator::filter_map. I want to argue that it’s useless and Rust provides more powerful tools to replace it.

🐸

This tweet post is inspired by this tweet from Boxy. Thanks, Boxy. <3

What is Iterator::filter_map?

So filter_map is a function provided by the Iterator trait:

fn filter_map<B, F>(self, f: F) 
    -> FilterMap<Self, F>
    // -> impl Iterator<Item = B>
where
    F: FnMut(Self::Item) -> Option<B>,

It is similar to filter and map combined (hence the name), but also lets you take ownership of the item when removing (filtering) it:

let mut filtered = Vec::new();
let unfiltered = iterator
    .filter_map(|x| match condition(&x) {
        true => Some(x),
        false => {
            // You couldn't do the same with just 
            // `filter` and `map` because `filter` 
            // only provides `&Item` while `map`
            // won't get the filtered elements.
            filtered.push(x);
            None
        }
    })
    .collect::<Vec<_>>();
🐸

This example could be better written using partition or partition_map, like this. Still, you may need ownership to do something with the value you are filtering.

So far, filter_map seems pretty useful, right? Well, turns out it’s just a less general version of…

flat_map to rule them all

flat_map is another function provided by the Iterator trait. It looks like this:

fn flat_map<U, F>(self, f: F)
     -> FlatMap<Self, U, F>
     // -> impl Iterator<Item = U::Item>
where
    F: FnMut(Self::Item) -> U,
    U: IntoIterator,

This function is equivalent to map combined with flatten (which, in turn, is equivalent to flat_map(id), they are interchangeable).

At first glance, it may seem like filter_map and flat_map are different: the first expects a function that returns Option while the other expects a function that returns something that can be turned into Iterator. But then, if you think about it, Option may be seen as a collection with 0 or 1 elements, and take is its next function (if you know how monads work, this might have been obvious). Moreover, Option actually implements IntoIterator! So… you can replace any call to filter_map with a call to flat_map and everything will continue to work just fine :flower:

We can’t just remove filter_map because backwards compatibility sucks. But I don’t think I’ll use it ever again.

Some more takeovers from Option: IntoIterator

iter::once(x) is actually equivalent to Some(x) in cases where IntoIterator is expected. It is even implemented using the option’s IntoIter. You probably shouldn’t use Some(x) like this, but you could.

[1, 2, 3]
     .iter()
     .copied()
     // :thinking:
     .chain(Some(special))
     .chain(iter::once(special))

Result is also IntoIterator

It behaves very like Option: If it’s Ok(_) it’ll yield exactly one item, otherwise, it won’t yield anything. Result::into_iter(res) is the same as Option::into_iter(res.ok()). I haven’t seen this impl used in practice. If you have an impl Iterator<Item = Result<T, E>> you can use .flatten() to ignore errors, I guess.

Concerns

There are some concerns about whatever flat_map can replace filter_map. I don’t think that they are significant, but they exist.

Readability

The most important of the concerns: with filter_map intent may be clearer to some readers. I think that flat_map doesn’t noticeably decrease readability, but that may be different to some other programmers, especially beginners.

Optimizations

Since filter_map is more specialized and less complicated than flat_map it’s possible that it can be better optimized. It’s unclear how much does it affect speed or if it’s possible to fix this with some specialization in standart library.

filter_map is also 32 bytes smaller than flat_map.

Type inference

Since filter_map is less general, it can help type inference. For example (playground):

// Compiles
iter.filter_map(|_| <_>::default()).map(|x: T| {});

// Fails
iter.flat_map(|_| <_>::default()).map(|x: T| {});

However, in practice, it seems like filter_map is usually used with explicit Option, so this doesn’t matter.

Criticism

🐸

This article received some criticism that needed to be addressed.

In response to it this paragraph was added in 2021-09-21 update.

Turns out filter_map has one advantage over flat_map — it can know its own size better. Since filter_map can’t ever add more elements than there were previously, its size_hint is (0, upper) (where upper is the upper bound of the inner iterator). flat_map’s size_hint on the other hand returns (0, None) in most cases (it’s a bit more complicated than that since flat_map stores iterators it can sometimes know a bit more).

It should be possible to specialize flat_map for Option<_> (and maybe Result<_>, etc) so it produces more accurate size_hint. However, at the moment of writing this there is no such specialization which may make flat_map considerably worse than filter_map in some scenarious. I’d like for filter_map to be fully equivalent to flat_map, however atm it simply isn’t.

Conclusion

I still prefer to use flat_map over filter_map, it seems right (also for some reason I really like the name). When making your choice between the two consider readability and size_hint (see above).

There are a lot of hidden things in Rust, which are hard to notice, but when you do notice them, you can only say “of course!” (for example Option: IntoIterator). I would recommend reading iterator docs carefully, there are a lot of hidden gems.

bye.