Function to take Iterator with a trait bound

I'm trying to make a function that accepts an object that I can call iter() on which will return an Iterator that can be used with the print! macro and display the object. I'm having issues on exactly how to define this. Most importantly, when I call this it can't move/destroy the object, so into_iter() is out.

What I want to have the body like is this:

fn print_iterable<T>(iterable: &impl IntoIterator<Item = impl std::fmt::Debug>) -> () {
    for cur in iterable.iter() {
        print!("{:?}, ", cur);
    }
}

This says that iterable doesn't have a .iter() method.

I can accept the idea that I may need to have the iterator as the direct argument so that my objects need to do .iter() when calling it, instead of inside the function itself. I thought I needed std::fmt::Debug for the {:?} part, but that may not be right.

I'm basically trying to do something similar to the following C++ code:

template <class iterable> void iterPrint(const iterable& input, std::ostream& ostr = std::ref(std::cout))
{
    ostr << "Contents: \"";
    for (auto& cur : input)
    {
        ostr << " " << cur;
    }
    ostr << "\"\n";
}

Though honestly, I'm thinking of something closer to IEnumerable<T> from C# as an equivalent.

I'm sure I'm missing something really basic here. I want to make a function that has an argument which is a reference to an object that implements a specific trait, and that trait has a bound which I need to specify as well. That it happens to be Iterator and Debug here isn't as important as knowing how to do this in general.

What is wrong in particular with this?

fn print_iterable(iterable: impl IntoIterator<Item = impl std::fmt::Debug>) {
    for cur in iterable {
        print!("{:?}, ", cur);
    }
}

It's a common convention for types that have a .iter() method to also implement IntoIterator for references to them. For example Vec<T> has a .iter() method, so &Vec<T> implements IntoIterator, and this means you can pass an &Vec<T> to this function. Rust Playground

If you're worried about being able to pass owned values in your functions, you can restrict it to accept only shared references, though to do that you won't be able to use the shorter impl Trait syntax. Rust Playground

2 Likes

Thank you for both of your answers. The reason I don't want the first (disallow ownership moving), is that both myself and others who are "not rust gurus" may be working and learning at the same time, and with the first form, you can pass in the not-referenced vec and then ownership moves.

Playground Link

Now the compiler will catch the use-after-move, but I want to by default disallow that if I can, and force it to be a read-only reference.

Thanks again for your answers. Your second in particular helps me put exactly what I need into "inner" definitions on Traits I'm using, and the lifetime parameter. I'll admit I even went through and read what rustc --explain E0637 gives, and I don't understand why I need the lifetime parameter there, but apparently I do, and you had that all, so thanks.

Edit: and I'll admit, I keep forgetting that if I pass a reference to a function, it can't mutate it, and that you can't "move" a reference (it clones it), and so that's fine. That's also part of it here

It's not generally a good reason for artificially restricting an API. It will cause frustration for the majority of the users who already know how to approach such an API. If it's a correct, reasonable, and foreseeable usage scenario for your function to be passed ownership, then you shouldn't actively prevent it from doing so.

Perhaps you understand better at this point, but what you actually wanted was

  • A reference that implements a specific trait (IntoIterator)
  • fn f<'a, T>(iter: &'a T) where &'a T: IntoIterator ...

And not

  • A reference to an object that implements a specific trait
  • fn f<T>(iter: &T) where T: IntoIterator ...
  • fn f(iter: &impl IntoIterator ...)

The latter would work if there was an Iterable trait with an iter(&self) method, but there isn't -- presumably because (a) the arguably "best" way to do so is with GATs (generic associated types), which aren't stable, and (b) with or without GATs, the higher-ranked and otherwise indirect bounds you need are convoluted and have normalization issues to boot.

I considered walking through making an Iterable trait, but (a) I think it would be more confusing than helpful and (b) the convoluted bounds required make it not worth it here IMO; just use one of @SkiFire13's suggestions.

If you show some code that gives the E0637 error, someone can probably explain it.

2 Likes

Thanks for the reminder that you can put bounds on references just like any other type! It's obvious when you sit down and think about it (and know you can put bounds on any type, not just parameters), but it's easy to forget references count too.

If you show some code that gives the E0637 error, someone can probably explain it.

Just the same as @SkiFire13 's 2nd example, just ditching the lifetime annotations gives that compiler error: Playground

Why the lifetime annotation is needed when the function returns nothing isn't clear to me, even when I went to the links that the compiler suggested.

Let me first note that &I isn't actually a type (even when I is fixed, e.g. &str). It's a type-constructor: You have to supply a specific lifetime for it to be a concrete type, and &I with different lifetimes are different types (even though you can often coerce the lifetime automatically).


Rust let's you elide lifetimes in various places, like function parameters, and it will have a certain meaning depending on the context. For example, these are pretty much the same [1]:

fn foo(s: &str) {}
// foo is higher-ranked (generic over the lifetime)
// When you call it, you call it with one specific concrete lifetime
//    (but you can choose any lifetime you please)
fn foo<'a>(s: &'a str) {}

But a normal trait bound isn't a place that accepts elided lifetime bounds.

So when we have a bound like so:

// E0637
&I: IntoIterator

It might mean you wanted a higher-ranked trait bound (HRTB) -- you wanted the bound to hold for any lifetime:

for<'any> &'any I: IntoIterator

Or it might mean you just need the bound to hold for a given specific lifetime:

// `'a` is declared elsewhere
&'a I: IntoIterator

There's no default in the case of normal trait bounds -- you can't elide the lifetime; you have to be explicit about which you meant.


For your use case, you only need &I: IntoIterator for whatever lifetime that gets passed in to your function, and that's what you get when you introduce an explicit lifetime name and tie them together:

fn print_iterable<'a, I, T>(iterable: &'a I)
where
    &'a I: IntoIterator<Item = T>,
    T: std::fmt::Debug,

You do have to tie them together; this won't work:

fn print_iterable<'a, I: 'a, T>(iterable: &I)
where
    &'a I: IntoIterator<Item = T>,
    T: std::fmt::Debug,
{
    for cur in iterable {
        print!("{:?}, ", cur);
    }
}

Because it's short for:

fn print_iterable<'a, 'b, I: 'a, T>(iterable: &'b I)
where
    &'a I: IntoIterator<Item = T>,
    T: std::fmt::Debug,

And you only restricted &'a I to be IntoIterator; you can't assume &'b I implements IntoIterator as well, much less that it implements it with the same Item type.

Remember: &'b I is a different type from &'a I.


Now I could continue on to demonstrate what happens when you try to go with the higher-ranked bound instead:

fn print_iterable</* ... */>(iterable: &I)
where
    for<'any> &'any I: IntoIterator /* ... */

But you don't need that capability here -- it would make your function less flexible -- and it would also go straight into the aforementioned convoluted bounds that tend to have normalization issues, so I'll hold off.


  1. the only difference is you can't name 'a in the first version ↩︎

2 Likes

@quinedot, I'm just surprised that the Elision Rules aren't OK with this. There's only one argument, and doesn't return a reference. Sure there may be HRTB shenanigans possible too, but by default we can't elide this? That's what's especially surprising for me here, that's all.

I don't think it comes up much; I think putting bounds on references to generic variables is pretty uncommon. Instead you either have bounds on a generic variable and take a reference to it (what you probably would have done if we did have an Iterable trait) or you let the reference itself be the generic variable (even if that means you can pass an owned value to it, which most people aren't concerned about preventing).

I.e. print_iterable(iterable: impl IntoIterator<...>).


Is it possible? It would only make code that doesn't compile now compile, so yes it's possible from a backwards compatibility point of view. I think this would be the first case of an elided lifetime parameter turning a higher-ranked function item into an early-bound parameter [1], so there may be technical challenges. And people may argue that the elided form should be higher-ranked in that position anyway.

My guess is that it's unlikely to happen as it's not a common enough pattern to have an obvious default or to spend the effort on, but I'm not on any teams and that's just a guess.


  1. not something most people are aware of, but this determines which of the following you can do.
    One of:

    • let _: fn(&[u8]) = print_iterable; // higher-ranked
    • let _ = print_iterable::<'static, u8>; // early-bound
    ↩︎
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.