Idea: add `Iterator::derefed`

This would be analogous to Iterator::copied and Iterator::cloned. It would simply apply Deref::deref to each item. I don't really like the name, but at least it's consistent.

I'm imagining an implementation like so:

use std::{ops::Deref, rc::Rc};

struct Derefed<I> {
    it: I,
}

impl<I> Derefed<I> {
    fn new(it: I) -> Derefed<I> {
        Derefed { it }
    }
}

impl<'a, I, T: 'a> Iterator for Derefed<I>
where
    I: Iterator<Item = &'a T>,
    T: Deref,
{
    type Item = &'a T::Target;

    fn next(&mut self) -> Option<Self::Item> {
        self.it.next().map(Deref::deref)
    }
}

// For testing
trait IterExt: Iterator {
    fn derefed(self) -> Derefed<Self>
    where
        Self: Sized
    {
        Derefed::new(self)
    }
}
impl<T: Iterator> IterExt for T {}

fn main() {
    let x = [String::from("Hello"), String::from("World")];
    
    let v: Vec<_> = x.iter().derefed().collect();
    assert_eq!(v, vec!["Hello", "World"]);

    let x = [Rc::new(1), Rc::new(2)];
    let v: Vec<_> = x.iter().derefed().collect();
    assert_eq!(v, vec![&1, &2]);
}

This could be especially useful when working with iterators of &String, but also iterators of Arc, Rc, etc.

Does this seem like a useful addition to the standard library?

To me, cloned() and copied() make sense as a way to build a new independent/owned list in a concise manner.

It's unclear to me how often it's needed to create an owned allocation of references, to justify adding derefed(). For the non-allocating case (for loop, instead of collect()) then deref should be automatically applied at the use site.

On naming consistency -- Option and Result also have cloned() and copied(), but they call this operation as_deref() and as_deref_mut(). It's not exactly the same though since we're not talking about the Iterator itself being treated "as ...".

2 Likes

Option has as_deref and as_deref_mut.

Yes, I much prefer the naming of as_deref, but as you say I wasn't sure that it should be applicable since it's not "the thing itself". They do use the same cloned/copied terminology so perhaps it would be fine.

That's fair – as you say in this case the new iterator would not be owned.

On the flip side, I don't think it's uncommon to want an iterator of &str, &[T], etc. Admittedly this can sometimes obviated by more flexible APIs (e.g. f(arg: impl Iterator<Item = impl Deref<Target = str>>)). It's also analogous to the as_deref methods available on Option, Result, etc.

My particular use-case was using chain to combine iterators, some of which were natively &str while others were &String. Unifying with derefed()/as_deref() rather than map(String::as_str) (e.g.) would be a pleasant convenience.

2 Likes

More bikeshedding: I prefer map_deref. It's consistent with Itertools in itertools - Rust . Maybe add this API to itertools first?

2 Likes

Option::as_deref applies Deref to the contained value (if any), so it would be consistent with Iterator::as_deref, certainly?

1 Like

"As" is usually a "one value → one value" operation, regardless of some type similes. But then perhaps .cloned should have been .map_clone as well.

1 Like

Is .map(Deref::deref) not enough? And where to draw the line? AsRef? From?

2 Likes

A drawback on any Map solution is that the full type can't be named.
(unless you indirect that through &dyn Fn, &mut dyn FnMut, or a fn pointer)

1 Like

Only because you can't name the function item type, which feels like something that should be possible, eventually. On nightly, you can define an alias for the type of Deref::deref using TAIT, and thus name the return type of iter.map(Deref::deref) as well:

1 Like

I guess the same could be argued for map(Clone::clone) etc. If it wasn't for the existence of as_deref on Option/Result I'd definitely say there's a bit of "slippery slope" to the suggestion. Since they do exist it feels to me like closing a gap.

The other ergonomic issue with map(Deref::Deref) is that Deref is not in the default prelude.

That's not a valid argument because the outcome is biased by backwards compatibility. I.e. even if we decided that you're correct and they shouldn't have been added they would still have to stay.

the existence of as_deref

That's necessary due to the &Option<T> to Option<&T> conversion (i.e. as_ref()) that has to happen first before you can deref. That doesn't apply to iterators because borrowing iterators are already by-ref.

I think it would be better to not cite analogies but instead explain how this adds value, and specifically more value than providing adapters for other traits such as AsRef, ToOwned, Into

2 Likes

That's all fair.

It does seem that the case for is a bit weak, particularly considering the "why not AsRef/ToOwned/..." question. I don't think there is any reason beyond familiarity from Option/Result, and as you say that's necessary to convert from &Option<T> to Option<&T>.

I guess the value-adds would be:

  • Avoid the import of Deref (not an issue for Clone/AsRef/Into/...). I'd say this is pretty low value, and could rather motivate adding Deref to prelude rather than adding more methods to Iterator.

  • Make it easier/possible to name the type of the returned iterator (e.g. vs .map). This is "nice to have" but given the same issue exists for all other conversation traits (apart from Clone/Copy) I'd say it's low value. Given the prevalence of impl Iterator/I: Iterator in signatures I'd guess it would be pretty niche use cases that would need to name the type.

  • "Knowledge transfer" from Option/Result to Iterator. While not always the case, there are some shared APIs between Option/Result/Iterator as discussed above , including .copied/.cloned. This is what prompted me to open the thread, as I half hoped/expected to find an Iterator equivalent of the very handy as_deref.

    Ultimately, though, Iterator doesn't need the &-to-owned aspect of as_deref that Option and Result require (though these are also provided by From) and so .map(Deref::deref) is essentially equivalent.

All said, I think I'll drop this :slight_smile: