collect::<Option<T>>() on an empty iterator

#1

I found very useful the property of FromIterator<Option<A>> for Option<V>, but I found a bit surprising how it works on an empty iterator:

println!(
    "{:?} should not this be None?",
    [Some("a".to_string()), Some("b".to_string())][..0]
        .iter()
        .cloned()
        .collect::<Option<String>>()
);  // Some("") should not this be None?

collect empty example

Having a look at the option implementation, if the iterator is empty self.found_none is never set at true, and Some(v) is returned, where:

let v: V = FromIterator::from_iter(adapter.by_ref());

In the case of String is v = "", so the result is Some("") and not None as I thought.

I started this journey just to avoid an empty-string-as-none problem, but this ruined my plans.

Can someone explain me the ratio behind this behaviour [^1]?

[^1]: And the previous one, before the option collect optimization, this is not a regression.

0 Likes

#2

This will be easier to explain if I change Option<String> to Option<Vec<T>>.

When you call collect::<Option<Vec<T>>> on an Iterator<Item = Option<T>>, then it will check for each item, is it None, if so return None, otherwise it is Some(_), then unwrap it and put it in the Vec<T>. This way, if you don’t have any values in the iterator, it doesn’t see any None, and so it doesn’t return None.

The important thing to note: calling collect::<Option<_>> will only return None if the iterator over Option<_> returns None.


i.e.

playground

fn main() {
    let arr: [Option<char>; 0] = [];
    assert_eq!(Some("".to_string()), arr.iter().cloned().collect::<Option<String>>());
    
    assert_eq!(Some("hi".to_string()), [
        Some('h'),
        Some('i')
    ].iter().cloned().collect::<Option<String>>());
    
    assert_eq!(None, [
        Some('h'),
        Some('i'),
        None
    ].iter().cloned().collect::<Option<String>>());
    
    assert_eq!(None, [
        None,
        Some('h'),
        Some('i'),
    ].iter().cloned().collect::<Option<String>>());
}

The reason for this behavior is because most collections (including String) in the std-lib allocate lazily, therefore an empty collection only takes up stack space. In many cases it is useful to lift an Option or Result and through a collection. i.e. going from Vec<Option<T>> to Option<Vec<T>> or Vec<Result<T, E>> to Result<Vec<T>, E>, which can be done like so: vec.into_iter().collect::<Option<Vec<_>>>().

0 Likes

#3

Thanks @KrishnaSannas,

I understand that, what I missed was the semantic of this behaviour, long story short, I hoped this code worked as solution for an exercism.io exercise:

fn raindrops_func(n: u32) -> String {
    let sounds = [(3, "Pling"), (5, "Plang"), (7, "Plong")];
    sounds
        .iter()
        .filter(|&(i, _)| n % i == 0)
        .map(|&(_, s)| Some(s))
        .collect::<Option<String>>()
        .unwrap_or(n.to_string())
}

fn main() {
    println!("raindrops({}) = {:?}", 1, raindrops_func(1));
    println!("raindrops({}) = {:?}", 10, raindrops_func(10));
}

collect::<Option<String>>() is Some if all items are something and None if at least one is nothing.

Even if an empty list is more nothing than something, Some is returned the same, because collect::<Option<T>>() is to be interpreted as collect_all (all() is true for an empty iter, like Some is returned in this case), and not as collect_any (any() is false for an empty iter, like None may be returned on an empty iter), so I had to add an if any() check and it worked.

0 Likes