Iterator api filter & map parameters

Newbie here with a question regarding this thread: Iterator API filter and map .

Why is map closure parameter not double referenced in the same way filter is? I understand the reasoning for filter, but ultimately the item received by the closure is a reference, isn't it?

I am coming from scala, and seeing that the filter closure parameter is double-referenced and the map closure parameter is single referenced is surprising, though I sort of understand the mechanisms related to borrowing/ownership.
tia

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.

6 Likes

Or, the short answer is that map() simply doesn't need to pass the item by reference, because it doesn't re-use it. filter() needs to reuse (yield or not yield) the next item after the predicate function returned, so it can't pass the item by value.

Generally, one should not pass stuff by reference just for the gigs of it. The default should be passing by value, and you should do anything else for a reason.

3 Likes