Can a single function work with .iter() and .map() as input?

I have a function f that operates on containers of Words. I would like to be able to call both f(vec.iter()) and f(vec.iter().map()). But vec.iter() returns an Iterator<Item = &Word>, whereas vec.iter().map() returns an Iterator<Item = Word>, so I can't write an input type for f that will work with both.

I tried f(words: Iterator<Item = dyn AsRef<Word>>), but that looked like it would require a cast at every call site (and may have issues with un-Sized local variables, but I can probably re-write the code to work around that).

Worst case, I can have two functions with different names and identical implementations, but it seems like there should be some way to do this. Am I missing something?

You probably need some knowledge about polymorphism in Rust. Not sure this helps, but I think you should read THE TENTH CHAPTER.

You could even make it work with f(&vec), too.

Something like impl IntoIterator<Item = impl std::borrow::Borrow<Word>> Rust Playground

1 Like

Thanks. I already knew most of what that chapter had to say, but your mention of it definitely nudged me in a helpful direction.

For anyone who stumbles on this in the future, what I started out with was something like

fn f<I>(words: I) -> Word
    where I: Iterator<Item = &Word>
{
    for w in words {
        do_stuff(w)
    }
}

which worked fine when I called f(vec.iter()), but failed to type check when I called f(vec.iter().map(...)) because my map was returning actual Words instead of &Words. The solution was to implement AsRef for Word and then change the function to

fn f<I, T>(words: I) -> Word
    where I: Iterator<Item = T>,
          T: AsRef<Word>
{
    for w in words {
        do_stuff(w.as_ref())
    }
}
1 Like

And after seeing steffahn's repsonse, I improved my function to the following:

fn f<I, T>(words: I) -> Word
    where I: IntoIterator<Item = T>,
          <I as IntoIterator>::IntoIter: ExactSizeIterator<Item = T>,
          T: AsRef<Word>
{
    for w in words.into_iter() {
        do_stuf(w.as_ref())
    }
}

and now I can call f(&vec) in addition to f(vec.iter()) and f(vec.iter().map(...)). (I didn't mention above that my actual function needs an ExactSizeIterator, but I'm including it here because it took me some time to figure out how to make it work.)

Thanks so much, both of you!

You should maybe consider using Borrow instead of AsRef. AsRef serves more as a conversion (that may modify behavior), while Borrow emphasizes on the "borrowing" (while keeping behavior such as comparison consistent).

There are some issues with AsRef, which make it sometimes unhandy to use. As you noticed, you have to provide an implementation of AsRef yourself, while Borrow<T> is automatically implemented for any type T (as well as &T).

Consider:

use std::borrow::Borrow;

fn takes_borrow<T: Borrow<i32>>(_arg: T) {}
fn takes_asref<T: AsRef<i32>>(_arg: T) {}

fn main() {
    takes_borrow(7);
    takes_borrow(&8);
    // The following two lines won't work,
    // and you can't make them work due to orphan rules:
    //takes_asref(7);
    //takes_asref(&7);
}

(Playground)

You can't make the last two lines work (due to the missing implementation and orphan rules).

Moreover, the way AsRef is implemented for some types in std is flawed (see my post on IRLO on that matter).

But these matters aside, I guess Borrow is semantically the better choice anyway. Not sure though.

1 Like

Semantically speaking, I consider using Borrow for "T or &T" a bit of a hack if you don't need the additional guarantees.

But if you just want "works with T and &T", Borrow is sufficient. And implementors of AsRef<T> who don't implement Borrow<T> [1] can always slap .map(AsRef::<T>::as_ref) on their iterator. [2] And iterators over &&Word more specifically can always slap on .copied().

If the goal is to maximize calling ergonomics for others, AsRef is more versatile as it removes the need for the iterator adapter. You do need the implementation yourself to work around AsRef's coherence-inflicted shortcomings.


  1. maybe the T is just one field out of many and Borrow is unsuitable to implement ↩︎

  2. The fact they can do this sort of highlights how there's no semantic benefit to Borrow here... you're doing the conversion to &T no matter what. ↩︎

2 Likes

Hmmm… :thinking: With my current understanding, I think that .borrow() is an abstraction of &, while .as_ref() isn't. The latter manifests in the requirement to provide reflexive implementations where needed, i.e. AsRef works only on sets of types which are to be convertible into each other (by reference). The consistency requirements of the former are due to how "borrowing" is supposed to work (i.e. you could consider them being part of PartialEq, etc. rather than originating from Borrow).

That is why I think using Borrow isn't a hack but the right abstraction. But I'm still not sure myself. Maybe it's difficult to get to a clear decision here. I guess you can use either, and it depends on what you want to achieve:

  • If you want to provide a generic interface that automatically performs a reference conversion to the particular needed type, then use AsRef. (In a similar fashion where you would use Into<T> to provide a generic, automatically converting API if you worked by-value.)
  • If you want to provide an abstraction over being able to borrow the value from the input, then use Borrow.

I think only the latter (i.e. Borrow) would allow you to be truly generic over T.

So the question is whether function f in the OP is supposed to work on "any value that is, by reference, convertible to &T" (then use AsRef<T>) or to work on "any value where you can borrow a T from" (then use Borrow<T>). Maybe it isn't even clear what's the intention here and perhaps it's just a matter of ergonomics in a particiular case and these semantic considerations are too far-fetched anyway?

A third alternative would be to provide your own trait to provide the abstraction:

struct Word;

trait GazeWord {
    fn gaze(&self) -> &Word;
}

impl GazeWord for Word {
    fn gaze(&self) -> &Word {
        self
    }
}

impl<T: ?Sized + GazeWord> GazeWord for &T
{
    fn gaze(&self) -> &Word {
        (*self).gaze()
    }
}

// The following would allow `Box`es, `Arc`s, etc. as well,
// but conflicts with the previous implementation and
// doesn't allow `&&Word`, `&&&Word` etc.:
/*
impl<T> GazeWord for T
where
    T: std::ops::Deref<Target = Word>,
{
    fn gaze(&self) -> &Word {
        &**self
    }
}
*/

fn f<T: GazeWord>(_arg: T) {}

fn main() {
    f(Word);
    f(&Word);
    f(&&Word);
}

(Playground)

But I guess then you can also just use AsRef, which, despite being broken in my opinion see also the docs on that matter (disclaimer: I updated those docs in PR #99460), at least contains some implementations to work with one level of most common smart-pointers such as Arc while allowing an arbitrary level of &'s. If you wanted to both support &&&&&Word as well as any SmartPointer<Word> with an own trait, you'd need to do a lot of manual implementations for all of these.

1 Like

Rethinking about this, I think if you want to abstract over an "arbitrary number of indirections" (i.e. Word, &Word, &&Word, etc.), then AsRef<Word> is the better choice, simply because:

the trait bound `&&Word: Borrow<Word>` is not satisfied

(Playground)

I'm not interested in writing reams about it, but what I meant about semantics is that they don't need the Hash/Eq/Ord constraints that Borrow exists for. They're just trying to end up with the input being a &T.

1 Like

I wonder if it's considered idiomatic to solve the problem of the OP by making f generic.

This makes me wonder: Is it possible to solve the problem if f is given to take an Iterator<Item = &Word>? I assume you can't even declare the function f properly with the Iterator trait, because the lifetime 'a of &'a Word cannot depend on the lifetime passed to Iterator::next, right? Is this what lending iterators or StreamingIterator is for? Or do I mix up something?

My question is: What's the idiomatic way to solve this problem in current (and possibly future) Rust?

1 Like