If you have an iterator of some item type Foo
, then a map
operation can transform this with a fn(Foo) -> Bar
function (or a closure with the same signature) into an iterator with items of type Bar
. Unlike Scala, Rust has an ownership system where an iterator with items of type Foo
would produce each item exactly once, and a function such as the one mapped by map
with signature fn(Foo) -> Bar
would fully consume items of type Foo
(and then produce ones of type Bar
which the new, mapped, iterator can then return).
It all works out directly like this with map
, but it’s different with filter
: When filtering, you need the filtering predicate to be able to inspect the item (in order to determine whether it’s discarded) and then the new, filtered iterator also still needs to be able to return the item afterwards. In Rust, we solve this with a borrow. The predicate is then of type fn(&Foo) -> bool
, not fn(Foo) -> bool
, in order to make sure that, since the Foo
value was only borrowed, after the execution of the predicate it still exists, so the filtered iterator can return it as an item.
This argument doesn’t quite prescribe the usage of an immutable borrow - a filter
operation with a fn(&mut Foo) -> bool
predicate would work, too, but immutable access better fits the intuitive understanding of a pure “filtering” operation. Furthermore, the argument that the item is still needed only applies in case the item isn’t discarded. One could imagine a predicate that receives the Foo
by-value, and only is required to give it back (potentially mutated, too) if it isn’t discarded. Kind of like fn(Foo) -> Option<Foo>
, where all None
-returning calls discard the item. Finally, this kind of transformation could even be allowed to change the type of the item, and we arrive at the more general API of filter_map which allows for fn(Foo) -> Option<Bar>
type “predicates”.
Now, why double-references? This happens when the iterator already has type &Baz
, as is common with many iterators returned by some collections’ .iter()
methods. For example Vec<T>
’s .iter()
method returns an iterator of &T
references.
The discussion above for the API design only applies in the general case, where the iterator’s item type cannot be assumed to necessarily be copyable. For iterators of &Baz
item type, they are copyable. The API of filter
and map
still stay the same for consistency - it’s hard (and also largely unnecessary) to try to insert special-cases into the typesystem for this [or special, separate API, etc…], so it just works the same as above. If instead of Foo
, we have &Baz
as an item type, then map
applies a fn(&Baz) -> Bar
mapped function, and filter
will have fn(&&Baz) -> bool
predicates; these types signatures are what you get by replacing Foo
with &Baz
in the type signatures presented in the first paragraphs of my answer.