2

I have a trait method that finds a reference to an element in a collection by linearly scanning through its elements.

I'd like to be able to implement this once for both Vec<Tag> and &'a [Tag] (and ideally support other iterable data structures too).

In the code below, the instances of TagFinder are identically implemented for Vec<Tag> and &'a [Tag], but I can't find a way to express this generically. Is it possible?

This other question seems relevant, but I have an extra level of indirection here in that I'm dealing with "iterables" and not iterators.

Relatedly, it seems it would be handy if there were a trait like IntoIterator that exposed an iterator of references (i.e. Vec<T> and &[T] would both iterate over &T, rather than Vec<T> exposing an owning iterator). I'm not sure why such a thing doesn't exist.

struct Tag {
    key: String,
    value: String,
}

trait TagFinder {
    fn find_tag(&self, key: &str) -> Option<&str>;
}

impl<'a> TagFinder for &'a [Tag] {
    fn find_tag(&self, key: &str) -> Option<&str> {
        find_tag(self.into_iter(), key)
    }
}

impl TagFinder for Vec<Tag> {
    fn find_tag(&self, key: &str) -> Option<&str> {
        find_tag(self.into_iter(), key)
    }
}

fn find_tag<'a, I>(tags: I, key: &str) -> Option<&'a str>
where
    I: Iterator<Item = &'a Tag>,
{
    tags.filter_map(|tag| match tag {
        &Tag {
            key: ref k,
            value: ref v,
        } if k == key =>
        {
            Some(v as &str)
        }
        _ => None,
    }).next()
}

fn main() {
    let v = vec![
        Tag {
            key: "a".to_owned(),
            value: "1".to_owned(),
        },
        Tag {
            key: "b".to_owned(),
            value: "2".to_owned(),
        },
    ];

    let s: &[Tag] = &v;

    assert!(v.find_tag("b") == Some("2"));
    assert!(s.find_tag("b") == Some("2"));
}

Edit

After some playing around I've come up with the following. It works, but I'm not really comfortable with why it works.

  1. The trait now consumes self, which would not be at all desirable, except for the fact that the only implementers of IntoIterator<Item = &'a Tag> seem to be borrowing types, so the self that is destroyed is only a reference. I'm a bit wary because there is nothing (except convention) stopping someone implementing that for an owning type like Vec.

  2. Moving the lifetime parameter from the method (elided) to the trait is weird. I'm finding it hard to understand how the return value ends up with a sensible lifetime.

  3. Why does v.find_tag(...) work? The receiver here is a Vec not a reference. How is Rust converting it to a reference?

Thanks. :)

trait TagFinder<'a> {
    fn find_tag(self, key: &str) -> Option<&'a str>;
}

impl<'a, T> TagFinder<'a> for T
where
    T: IntoIterator<Item = &'a Tag>,
{
    fn find_tag(self, key: &str) -> Option<&'a str> {
        find_tag(self.into_iter(), key)
    }
}
Ben Challenor
  • 3,295
  • 32
  • 35

1 Answers1

3

How to implement trait once for all iterables of &T

Pretty much as you've specified:

trait Foo {}

impl<'a, T: 'a, I> Foo for I
where
    I: Iterator<Item = &'a T>,
{
}

You can substitute IntoIterator for Iterator if you'd like.

For your specific case:

trait TagFinder<'a> {
    fn find_tag(self, key: &str) -> Option<&'a str>;
}

impl<'a, I> TagFinder<'a> for I
where
    I: IntoIterator<Item = &'a Tag>,
{
    fn find_tag(self, key: &str) -> Option<&'a str> {
        self.into_iter()
            .filter(|tag| tag.key == key)
            .map(|tag| tag.value.as_ref())
            .next()
    }
}

The trait now consumes self, which would not be at all desirable, except for the fact that the only implementers of IntoIterator<Item = &'a Tag> seem to be borrowing types, so the self that is destroyed is only a reference. I'm a bit wary because there is nothing (except convention) stopping someone implementing that for an owning type like Vec.

If you can find some way to take an owning value and return a reference to it, then you've found a critical hole in Rust's memory safety. See Is there any way to return a reference to a variable created in a function?.

Moving the lifetime parameter from the method (elided) to the trait is weird. I'm finding it hard to understand how the return value ends up with a sensible lifetime.

I don't understand the confusion. You've explicitly specified the lifetime, so in what manner would it not be reasonable? You aren't removing it from the method, you are just adding it to the trait because now the trait has to know that 'a is something from "outside" the trait itself.

Why does v.find_tag(...) work? The receiver here is a Vec not a reference. How is Rust converting it to a reference?

The same way any other method call that takes a reference works when called on a value. See What are Rust's exact auto-dereferencing rules?

Shepmaster
  • 274,917
  • 47
  • 731
  • 969
  • Thanks for the follow up. Good point regarding (1) - I didn't consider that. Regarding (2), in the original formulation the lifetimes of `&self` and `Option` are directly linked (albeit via elision). In the new formulation, my intuition is that any useful `self` type will contain a reference to `'a` and thus have the same lifetime as `Option`, but I am not sure how to formalize that. Regarding (3), is there any way to get the compiler to dump a fully-qualified AST so I can know what it did? In particular, I'm not sure if it's using the `&'a Vec` or `&'a [T]` instance. – Ben Challenor Jan 08 '18 at 15:32
  • 1
    @BenChallenor (2) Yes, any correct implementation of the trait will involve `Self` having some reference with the lifetime `'a`. I don't know how to formalize that for you, but it's a consequence of the first part — you cannot "magic up" a reference that wold life long enough (3) No, but that's why I linked to the question I did. It will try `Vec`, `&Vec`, `*Vec`, then `&*Vec` in order, picking the first that implements it. – Shepmaster Jan 08 '18 at 15:40
  • thanks. (2) The worst I can come up with is implementing the trait for `()` by returning a `'static` reference, but to be fair you can do this with the original formulation too. You certainly can't return the key `&str`. So I think I understand this now. (3) I think I'll have to try some more examples to get a feel for the auto-dereferencing rules, but I'm happy with this answer. – Ben Challenor Jan 08 '18 at 22:19
  • 1
    @BenChallenor If an iterator emits `'static` references, then it will only implement `TagFinder`, not `TagFinder` for any other lifetime `'a`. – Francis Gagné Jan 09 '18 at 01:45