Flat_map vs map and flatten vs something else?

The docs for flat_map explicitly describe Iterator::flat_map as being like a combined map and flatten, but when I try to do the obvious substitution I get compiler errors. So I start with this which compiles:

    if let Some(path) = delta
        .new_file()
        .path()
        .map(|x| x.file_name())
        .flatten()
        .map(|x| x.to_str())
        .flatten()

Where delta.new_file().path() returns an Option<&Path>. This fails to compile:

    if let Some(path) = delta
        .new_file()
        .path()
        .flat_map(|x| x.file_name())
        .flat_map(|x| x.to_str())

With an error about Option not being an Iterator -- which is confusing because Option has methods like map. So then I try:

    if let Some(path) = delta
        .new_file()
        .path()
        .iter()
        .flat_map(|x| x.file_name())
        .flat_map(|x| x.to_str())

Closer. Now it complains that the right hand expression is a nested FlatMap<...> structure and the left hand destructuring is looking for an Option. So then I do:

    if let Some(path) = delta
        .new_file()
        .path()
        .iter()
        .flat_map(|x| x.file_name())
        .flat_map(|x| x.to_str())
        .next()

And now finally it's happy. However this feels really really really bizarre, like I'm playing combinator tetris to try to make rustc happy rather than writing anything I could expect a coworker to be able to read later, and treating Option as a 1 element container feels weird, and manually calling next on an iterator feels weird, and it's not obvious to me why to prefer one over the other or if there's a better way all together. I got here starting from having a couple nested if let pushing my code all the way to the right side of the screen, and maybe I should have stuck with that? I know if-let chains are coming.

It ends up being wierd because you are trying to use iterator methods on an Option. Option doesn't have a flat_map(), because the corresponding method is called and_then() to match with and() and or() (which don't really exist for iterators due to Iterator and Option having different uses).

Basically and_then() what you should be using instead of flat_map() here:

if let Some(path) = delta
        .new_file()
        .path()
        .and_then(|x| x.file_name())
        .and_then(|x| x.to_str())
2 Likes

Just because Option has some methods with the same names as the iterator methods (map, flatten), does not make Option an iterator, and as such, does not mean that it has all of the iterator methods.

3 Likes

Option is an IntoIterator which is the magic that makes flat_map work as expected on an iterator of Options. If you want to turn an Option into a real Iterator explicitly, you can call into_iter, iter or iter_mut.

1 Like

Ah, I keep running into not intuiting which result/option/iterator method I need. Now that I know this is the answer it reads much better though. Thanks!

Yep this is what I eventually figured out and why I eventually called iter(). It's still a bit confusing, because Option::<T>::map behaves the same as the Option<T> impl of Iterator::map and if you just see x.map() without thinking about the types it could be either. I'm guessing this is a more common pattern, maybe I'll get used to it.

Yeah, there is indeed a common pattern, and it originates from functional programming languages such as Haskell, and ultimately from a particularly abstract branch of math called category theory.

Any type that has a "map-like" operation is called a functor; any type that additionally has a "flat_map-like" operation is called a monad. Option and Iterator are textbook examples of monadic types. In current Rust, Option and Iterator are only "monadic by convention" – there's no common "Monad" trait due to type system restrictions (and it's not clear if the abstraction would even make sense in Rust).

3 Likes