Trait method to return an Iterator of borrowed `str`

There are a number of similar posts to this, but none that I've found have the particular quirk of my use-case.

I'm attempting to define a trait whose method returns an Iterator of &str. A basic skeleton would be:

pub trait Stringy {
    fn strings(&self) -> ???
}

where ??? should be some type that reflects the constraint correctly, namely Iterator<Item = &str>.

We can't impl Iterator<Item = &str> here because the "impl Trait" pattern isn't allowed here. Nor is setting this to a generic I: Iterator correct, because then the caller can define what the Iterator is. Since I don't want to have any type (or lifetime) params on Stringy itself, it seems like an associated type is what's necessary here.

pub trait Stringy {
    type Iter: Iterator<Item = &str>;

    fn strings(&self) -> Self::Iter;
}

This doesn't work because there's no lifetime on the &str. The compiler suggests we add a constraint to the associated type:

pub trait Stringy {
    type Iter<'a>: Iterator<Item = &'a str>;

    fn strings<'a>(&'a self) -> Self::Iter<'a>;
}

but then fails, telling me that constraints on associated types aren't stable yet.

Is it possible to do what I'm doing, or do I have to wait for the feature to stabilize?

Thanks.

The "straightforward" way, involving unstable features, would be to use a GAT ("generic associated type").

#![feature(generic_associated_types)]

pub trait Stringy {
    type Iter<'a>: Iterator<Item = &'a str>
    where
        Self: 'a;

    fn strings(&self) -> Self::Iter<'_>;
}

It's possible to work around the fact that GATs with lifetime arguments though. Following a recipe of mine (that @quinedot apparently likes to link to a lot, so let me do the same myself), you could translate this thing into something like

pub trait HasStringyIter<'a, _Outlives = &'a Self> {
    type StringyIter: Iterator<Item = &'a str>;
}

pub type StringyIter<'a, T> = <T as HasStringyIter<'a>>::StringyIter;

pub trait Stringy: for<'a> HasStringyIter<'a> {
    fn strings(&self) -> StringyIter<'_, Self>;
}

In this case it could however also be sufficient to just use a lifetime-parametrized trait, e.g.

pub trait Stringy<'a> {
    type Iter: Iterator<Item = &'a str>;
    fn strings(&'a self) -> Self::Iter;
}

depending on your use-case. You might still need the trick with the _Outlives parameter if you still want to use HRTBs (like T: for<'a> Stringy<'a>) for types that are non-T: 'static. (TL;DR, it's not unlikely you wont need the trick with the _Outlives parameter.) It's not possible to tell which solution is "the best" since you haven't provided any example types that implement the trait and example use-cases.

4 Likes

What I really want is the GAT approach, since it's clearly the "real" to do it, but I suppose that means using nightly for now then eh.