How to implement a trait for iterables of a type

In the code below, Record and Row are struct types. I want to implement the trait Printer for any container type (array, slice, vec, deque etc.) that can be iterated upon and contains Record (or Row).

Here is a playground.

struct Record {
    data: i32,
}

struct Row {
    data: i32,
}


trait Printer<T>
where
T: Iterator 
{
    fn print(&self) {
        for item in self {
            println!("{:?}", item.data);
        }
    }
}

impl Printer<[Record]> for [Record] {}
// ^^^^^^^^^^^^^^^^^ `[Record]` is not an iterator
// ^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time

impl Printer<[Row]> for [Row] {}
// ^^^^^^^^^^^^^^ `[Row]` is not an iterator
// ^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time

I think you're looking for something like this:

trait Printer {
  fn print(&self);
}

impl<T, I> Printer for I
where I: Iterator<Item = T>,
       T: Debug,
{
  fn print(&self) {
    for item in self {
      println!("{:?}", item);
    }
  }
}

When writing this sort of "extension trait", you'll typically write the trait definition then explicitly implement it for the type being extended (i.e. an iterator of Debug items).

1 Like

Above plus probably you want IntoIterator instead of Iterator.

4 Likes

Thanks, the suggestion does work but fails if I move the implementation of print to the trait itself such that the trait provides a default implementation of the function.

The code below errors out (errors in comments):

trait Printer {
  fn print(self) {
    for item in self {
             // ^^^^ `Self` is not an iterator
             // ^^^^ doesn't have a size known at compile-time
      println!("{:?}", item);
    }
  }
}

impl<T, I> Printer for I
where 
I: Iterator<Item = T>,
T: std::fmt::Debug,
{}

If you don't plan to use this trait for dynamic dispatch (i.e. through trait objects), you should simply add where Self: Sized to the method definition. If you do plan so, however, maybe you should think whether the print method must really consume self, or it could go with borrowing.

With the changed code at the bottom, it seems, I am getting close. The new problem, however, is the compiler is now complaining:

 no field `data` on type `T`

I cannot use T: Record because it is a struct and not a trait. How do I convince the compiler that T will always be a struct having a field data?

Also how do I make the following more generic to work on arrays, slices etc. in addition to Vec<>?

impl Printer<Record> for Vec<Record> {}

When I change to impl ... for [Record] it complains that [Record] is not an iterator but the code is asking for IntoIterator which [Record] (a slice?) seems to be.

struct Record {
    data: i32,
}

trait Printer<T>
where Self: IntoIterator<Item=T>
{

    fn print(&self) {
        for item in self.into_iter() {
            println!("{:?}", item.data);
        }
    }
}

impl Printer<Record> for Vec<Record> {}

That's why I was putting the implementation in the impl Printer for ... {} block.

If you provide a default implementation then that essentially hard-codes the contract to require something that can be iterated over, instead of something which can print itself... If that makes sense? If the default implementation exists, all implementors of the Printer trait would need to satisfy the conditions required by that default implementation.

You'd get that for free using my example from before. You just need to swap I: Iterator<Item = T> with I: IntoIterator<Item = T> so it works for anything which can be turned into an iterator, not just iterators. This includes iterators because std contains a impl<I, T> IntoIterator<Item = T> for I where I: Iterator<Item = T> { ... }

1 Like

Thanks for clarifying, appreciate it! Indeed, could get your solution going after resolving some compiler nit-picks.

Below is a working copy of the solution if someone lands here searching for similar problem.

#[derive(Debug)]
struct Record {
    data: i32,
}

trait Printer {
    fn print(self);
}

impl<T, I> Printer for I
where
    I: IntoIterator<Item = T>,
    T: std::fmt::Debug,
{
    fn print(self) {
        for item in self {
            println!("{:?}", item);
        }
    }
}

pub fn main() {
    let recs = [Record { data: 32 }, Record { data: 16 }];
    recs.print();
}

Note that this solutions is more general than what you were looking for (whether that's a good thing or a bad thing depends on what you intended to do): you will notice that Vec<i32> : Printer, and i32 != Record.

A solution that targets iterables (IntoIterator) of Records (Item = Record) would thus be written as follows:

struct Record {
    data: i32,
}

struct Row {
    data: i32,
}


trait Printer : Sized {
    fn print (self);
}

impl<Iterable> Printer for Iterable
where
    Iterable : IntoIterator<Item = Record>,
{
    fn print (self)
    {
        for record in self {
            println!("{:?}", record.data);
        }
    }
}

Now, the issue with this implementation is that, for instance, it won't work with &'_ [Record], since that's an iterable over &'_ Record, which is not Record.

You can circumvent that limitation with the Borrow trait (T : Borrow<U> if a T can be borrowed as a U, so T : Borrow<T>, &T : Borrow<T>, etc.):

use ::core::borrow::Borrow;

impl<Iterable> Printer for Iterable
where
    Iterable : IntoIterator,
    Iterable::Item : Borrow<Record>,
{
    fn print (self)
    {
        for elem in self {
            let record: &Record = elem.borrow();
            println!("{:?}", record.data);
        }
    }
}

And now we can check:

const _: () = {
    fn is_printer<T>
    () where
        T : Printer,
    {}

    is_printer::<Vec<Record>>;
    is_printer::<&[Record]>;
    is_printer::<&mut [Record]>;
    is_printer::<::std::collections::HashSet<Record>>;
    // is_printer::<Vec<i32>>; // Error
};
2 Likes

Thanks for the detailed write-up. :pray: I was already trying to work my way through the &'_ [Record] case and this was really helpful.

1 Like