Declaring associated item whose references implement IntoIterator


#1

I have a trait DataSource with a method data that returns an associated type called Data. Initially I wanted to force Data to provide an iter method, but I couldn’t find a standard trait with that method, so I tried requiring that for<'a> &'a Data: IntoIterator. For some reason, it seems like only the default implementation of IntoIterator for types that already implement Iterator is considered. I’m guessing this has to do with the HRTB and the lifetimes of the data actually stored in Data.
So… what’s the rusty way of doing this? The semantics I want are just a type that can be iterated over by borrowing, but it would still be nice why my approach failed.


#2

Here’s what I came up with - not sure if that’s what you had in mind:

trait DataSource
where
    for<'a> &'a Self::Data: IntoIterator,
{
    type Data: ?Sized;

    fn data(&self) -> &Self::Data;
}

struct S(Vec<i32>);

impl DataSource for S {
    type Data = Vec<i32>;

    fn data(&self) -> &Self::Data {
        &self.0
    }
}

impl<'a, D> DataSource for &'a D
where
    D: DataSource,
    for<'b> &'b D::Data: IntoIterator,
{
    type Data = D::Data;

    fn data(&self) -> &Self::Data {
        (**self).data()
    }
}

fn foo<D, I>(d: D)
where
    D: DataSource,
    for<'a> &'a D::Data: IntoIterator<Item = &'a I>, // extra requirement on the item itself so we can display
    I: std::fmt::Display,
{
    for x in d.data() {
        println!("{}", x);
    }
}

fn main() {
    let s = S(vec![1, 2, 3]);
    // direct use
    for x in s.data() {
        println!("{}", x);
    }
    // Couple of uses via generic fn
    // call with a reference
    foo(&s);
    // call with an owned value
    foo(s);
}

play

What code did you try? Please put it in a playground so people can play with/view it.


#3

I tried recreating the problem from scratch, but couldn’t get the compiler to reject it. Here’s a heavily minimised version of the of the code I’m working on, which still exhibits the same errors: https://play.rust-lang.org/?gist=8564436094e9df8cf6bafb22a933cae6&version=stable&mode=debug
I’d really like to understand how to interpret these error messages. It seems like the compiler is doing quite a few leaps of logic before spitting out a message, and unfortunately I cannot fully follow it in my head…


#4

Here is a fixed up version of what you had. A couple of main changes:

  1. Need to repeat the HRTB bound in a few places - it doesn’t “carry over” automatically (i.e. not implied)
  2. Need an additional constraint on the IntoIterator::Item where you expect the actual item type to be something specific (e.g. a tuple).

Note that I only got it to build - not sure if it’ll actually work when you go to make use of it :slight_smile:.


#5

Thanks! I don’t think I would have figured that out on my own. I guess there is some finer point I’m not quite getting, because so far my mental model of how traits work I always assumed that anything declared in a trait is assumed to hold for any type parameter that is bound by that trait. Is this not the case? For which kinds of bounds this is the case? And what’s the reasoning for this behaviour?


#6

This is slightly nuanced with traits. Best shown with a few examples.

use std::fmt::Debug;

trait MyTrait {
    type Assoc: Debug;

    fn get(&self) -> Self::Assoc;
}

fn take_trait<T: MyTrait>(t: T) {
    println!("{:?}", t.get());
}

This is a trait with an associated type that’s bound to Debug. This bound is automatically carried over to use sites, so we don’t need to repeat that bound in take_trait().

trait MyGenTrait<T>
where
    T: Debug,
{
    fn get(&self) -> T;
}

fn take_gen_trait<U: Debug, T: MyGenTrait<U>>(t: T) {
    println!("{:?}", t.get());
}

That’s a trait with a generic type parameter bound to Debug. You can’t impl that trait if the chosen T isn’t Debug. However, at the use site take_gen_trait(), you need to re-state that U: Debug even though that’s implied.

The “fun” example is a trait with an associated type that has an HRTB bound:

trait MyTraitWithHRTB where for<'a> &'a Self::Assoc: Debug {
    type Assoc;
    
    fn get(&self) -> &Self::Assoc;
}

fn take_trait_hrtb<T: MyTraitWithHRTB>(t: T)
where
    for<'a> &'a T::Assoc: Debug,
{
    println!("{:?}", t.get());
}

Now you need to repeat the same HRTB bound. I’ve no idea why that’s the case, but recall that happening when I tried this before.

This whole notion of “implied bounds” has an RFC for it. There’s an argument to be made that implied bounds create a bit of magic-at-a-distance feel since the implication chain can be non-trivial and local reasoning becomes much more difficult. For trivial cases like this, however, it seems like boilerplate.


#7

Thanks for taking the effort to answer so thoroughly. :slight_smile:

I guess my confusion comes from the fact that my original where clause was only constraining implementations of the trait, whereas bounds on type parameters constrain the set of parameters that users of the trait supply. Is there a different syntax, similar to type Assoc: Debug;, that would carry over the constraint? I tried something like type Assoc where for <'a> &'a Self::Assoc: Debug;, but that didn’t work either. (On top of that, rustfmt just deleted that where clause completely.)


#8

There’s no syntax to carry over constraints, but having this done automatically is what that RFC is about. Associated type constraints (but not the HRTB flavor!) happen to carry over today, but as you can see from the examples and that RFC, a bunch of other scenarios do not.

I’m not sure if that answers your question?


#9

I guess it does, it’s just still somewhat confusing. Thanks! :slight_smile:

I ran into a related problem when trying to write mock implementation for the trait. Luckily, it’s reproducible with a much shorter code sample:

trait Trait
where
    for<'a> &'a Self::IntoIter: IntoIterator<Item = u32>,
{
    type IntoIter;
    fn get(&self) -> Self::IntoIter;
}

struct Impl(Vec<u32>);

impl Trait for Impl {
    type IntoIter = ImplIntoIter;
    fn get(&self) -> Self::IntoIter {
        ImplIntoIter(self.0.clone())
    }
}

struct ImplIntoIter(Vec<u32>);

impl<'a> IntoIterator for &'a ImplIntoIter {
    type Item = <Self::IntoIter as Iterator>::Item;
    type IntoIter = std::iter::Cloned<std::slice::Iter<'a, u32>>;
    fn into_iter(self) -> Self::IntoIter {
        (&self.0).into_iter().cloned()
    }
}

Playground link
If I change the definition of Item to be simply u32 the error message disappears, but I don’t really understand why that happens.


#10

I don’t either :slight_smile:.

It seems like a bug to me.


#11

@ytausky can you please file a github issue for this latter error that you and I don’t understand? I’d really like to know what’s up with it myself.


#12

Sure, I wasn’t going to just let it go either! :wink: