16

I was trying to iterate over a subsection of a vector of strings, i.e. a subslice of Vec<String>. Within each iteration, I wanted to pass the string as a slice to a function.

I didn't notice that Vec::get returns an Option, and thought I could just directly iterate over the return value:

fn take_str(s: &str) {
    println!("{}", s);
}

fn main() {
    let str_vec: Vec<String> = ["one", "two", "three", "uno", "dos", "tres"]
        .iter()
        .map(|&s| s.into())
        .collect();
    for s in str_vec.get(0..3) {
        take_str(&s);
    }
}
error[E0308]: mismatched types
  --> src/main.rs:11:18
   |
11 |         take_str(&s); // Type mismatch: found type `&&[std::string::String]`
   |                  ^^ expected `str`, found `&[String]`
   |
   = note: expected reference `&str`
              found reference `&&[String]`

I was expecting s to be a String, but it's actually &[String]. This is because my for loop is iterating over the Option returned by Vec::get().

I also wrote the following code, which demonstrates that the for loop is in fact unwrapping an Option:

let foo = Option::Some(["foo".to_string()]);
for f in foo {
    take_str(&f); // Same error as above, showing `f` is of type `&[String]`
}

This is incredibly confusing; I never expected (until I wrote this code and figured out what it's actually doing) that Option could be unwrapped by iterating over it. Why is that supported? What use case is there for iterating over an Option?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Kyle Strand
  • 15,941
  • 8
  • 72
  • 167

1 Answers1

19

What use case is there for iterating over an Option?

My favorite reason, in a word, is flatten:

fn main() {
    let results = [Some(1), None, Some(3), None];
    let sum: i32 = results.into_iter().flatten().sum();
    println!("{}", sum)
}

Before Rust 1.29, you can use flat_map:

fn main() {
    let results = vec![Some(1), None, Some(3), None];
    let sum: i32 = results.into_iter().flat_map(|x| x).sum();
    println!("{}", sum)
}

Option can be thought of as a container that can hold exactly zero or one elements. Compare this to a Vec, which can hold zero or many elements. In a large set of ways, an Option is a container just like a Vec!

Implementing IntoIterator allows Option to participate in a larger share of APIs.

Note that IntoIterator is also implemented for Result, for similar reasons.

This is incredibly confusing

Yes, it is, which is why Clippy has a lint for it:

warning: for loop over `str_vec.get(0..3)`, which is an `Option`. This is more readably written as an `if let` statement
  --> src/main.rs:10:14
   |
10 |     for s in str_vec.get(0..3) {
   |              ^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(clippy::for_loops_over_fallibles)]` on by default
   = help: consider replacing `for s in str_vec.get(0..3)` with `if let Some(s) = str_vec.get(0..3)`
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#for_loops_over_fallibles

This shows that there are ways that an Option is not like a container to a programmer.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • "`Option` can be thought of as a container that can hold exactly zero or one elements." So...semantically, it's kind of like a `Box` but its owned object is entirely contained instead of heap-allocated? That's...not really the semantics I expected from `Option`, but I guess it makes sense. To be absolutely clear, your `flat_map(|x| x)` is equivalent to something like `map(|x| x.unwrap_or(0))`, correct? – Kyle Strand Apr 07 '17 at 19:40
  • 4
    Why `flat_map()` instead of `filter_map()`? I'd say the latter is more fitting in this situation... – Lukas Kalbertodt Apr 07 '17 at 19:44
  • 1
    @KyleStrand the heap-allocation aspect is orthogonal, really. `Vec` uses the heap to have dynamic capacity. There are stack-based collections too, usually with a fixed capacity. I only half-heartedly agree with the `unwrap_or` comparison. In *this* example, yes, they have the same result (due to how addition on integers works), but using `collect` instead of `sum` would show how they are different. – Shepmaster Apr 07 '17 at 19:48
  • 2
    @KyleStrand: A friendly reminder that `Box` always has an object in Rust, there is no null `Box` :) – Matthieu M. Apr 07 '17 at 19:50
  • @LukasKalbertodt that's a tough question, which has been [discussed at least once before](https://www.reddit.com/r/rust/comments/4xkat3/why_filter_map_and_flat_map/?ref=share&ref_source=link). *My* answer is because I like me some functional programming and monads, so I think of `flat_map`. The linked discussion suggests that `filter_map` is less powerful than `flat_map` and thus `filter_map` is the redundant one. – Shepmaster Apr 07 '17 at 19:51
  • Hm, you're right that `collect` would be harder to use with `map` instead of `flat_map` in this case. I agree that with @LukasKalbertodt that `filter_map` seems even better, though. What's not "functional" about it? And doesn't it still involve a monad? – Kyle Strand Apr 07 '17 at 19:54
  • And @MatthieuM.'s point is, I think, the real reason my `Box` comparison doesn't work (I forgot that C++'s `unique_ptr` is actually more like an unsafe `Option` than mere `Box`). – Kyle Strand Apr 07 '17 at 19:54
  • @KyleStrand: exactly my point yes; memory layout apart, `Box` only represent a subset of `Option` possible states: it's missing exactly one state, the empty one. – Matthieu M. Apr 07 '17 at 19:56
  • 1
    @KyleStrand Sorry, I don't mean to suggest that one is *more* functional than the other, just that the combined action "flat-map" is the name of something that exists in a functional language like Clojure, while "filter-map" is not, based on my experience. Usually it's composed of separate `filter` and `map` operations. Rust combines the two for ownership reasons. – Shepmaster Apr 07 '17 at 20:01
  • 1
    Additionally, if you choose to use `filter_map`, you don't get the removal of `Result` for "free"; you have to explicitly convert to an `Option`. – Shepmaster Apr 07 '17 at 20:01