Lifetimes on Associated Types

I've got this trait...

trait Consume {
    type O;
    fn f<'a>(&self, &'a [u8]) -> Self::O;
}

This is really the ideal way I want to define this trait. Anything that impls Consume knows how to take a slice and produce some O in the type family. Good. But I've got this weird struct...

struct Ident<C: Consume>(C);

impl<C: Consume> Consume for Ident<C> {
    type O = &'a [u8];
    fn f<'a>(&self, s: &'a [u8]) -> &'a [u8]  {
        &s[1..]
    }
}

Which does something very benign to the slice in question. Now unfortunately, this doesn't compile, because I haven't declared the lifetime 'a. But if I move it up to the impl, where it will be in scope for the associated type, I suddenly get a compiler error that my lifetime is not constrained by anything! It's a Catch-22!

Really, what I'm looking for is a way to say "this has a type that is related to some other generic function", without touching the trait definition itself. Does for syntax help here? Or should I give Ident some phantom data to hold on to the lifetime? But I wouldn't want to do that, either, because the lifetime of Ident actually has nothing to do with the lifetime of the struct!

I am surely confused.

2 Likes

If the lifetime needs to be referenced in the associated type, it had to be the trait's parameter:

trait Consume<'a> {
    type O;
    fn f(&self, &'a [u8]) -> Self::O;
}

Relevant: Implementing FnMut with lifetime

2 Likes

This is basically higher-kinded types; the associated type is parameterised by the lifetime, as there's no connection between the lifetime declared in the function and the one in the type (as you noted). A proposed syntax for the functionality is something like:

trait Combine {
    type O<'x>;
    fn f<'a>(&self, &'a [u8]) -> Self::O<'a>;
}

// ...

struct Ident<C: Consume>(C);

impl<C: Consume> Consume for Ident<C> {
    type O<'x> = &'x [u8];
    fn f<'a>(&self, s: &'a [u8]) -> &'a [u8]  {
        &s[1..]
    }
}

Unfortunately, this doesn't work now.

The for syntax is a little bit related (although it is handling higher-rankedness, rather than high-kindedness), but it doesn't work for that exact code. However, it is helpful if one moves the lifetime to the trait, as @gkoz demonstrates, then a bound that was T: Combine should be written T: for<'a> Combine<'a> and should (I think) achieve the same purpose.

2 Likes

It's so obvious that this is a higher kindled type that it didn't even occur to me until now. Than you both. I will add the parameter to the trait, for now, but it is unfortunate that every implementer of the trait has to worry about the lifetime. For syntax does help in that case, but the types still look messy. :confused:

What's the reason for needing generic items to be constrained by the top part of a trait rather than, say, items?

3 Likes

I'm not sure what you're saying here and can't parse

impl Consume for MyStruct where for<'a> MyStruct: Consume<'a>

Also I'd encourage you to drop the lifetime in

fn f<'a>(&self, s: &'a [u8]) -> Self::O

because it's a more noisy equivalent of

fn f(&self, s: &[u8]) -> Self::O

I was actually incorrect, I didn't check if that compiled.

Anyway, I can't do that, because Self::O in this case has the same lifetime as s, so the entire trait needs to be generic.

It's not so bad.

If the parameter is on the trait, sure, but if it's on the function and used exactly once it's just noise.