Don't Want a Lifetime in (Generic) Associated Type to Get ByRef

I'm trying to deal with the fact that ndarray views introduce an explicit lifetime into the type signature (such as ArrayView1<'a, A>). This is a bit of an odd issue as I want to specify the type in a generic associated type (giving the A). And I really just want the view for immediate use, such as with math ops which directly return a new owned type.

So, for the other types I'm using, I can happily impl<'a> Mul<&'a T> for &'a T with Output = T and never have to worry about 'a in T (or like how indexing returns &Self::Output). But trying to get an ndarray view that only ever needs an anonymous lifetime (because it will only come from an &self method) is proving difficult.

My current (lazy) answer is to specify the type as the owned version, and just get it by passing the view through a math op (view * 1.0 or + 0).

My other thought is to create a supertrait for views:

trait OwnedTypes: for<'a> ViewTypes<'a> { .. }

But that might just be push the problem of the named lifetime somewhere else.

Can you give an example that demonstrates the difficulty?

1 Like

I suppose difficulty isn't the right way to put it, because I could put a lifetime specifier on everything and it would work. Perhaps containing the spread of a named lifetime would be a better way to put it.

Function lifetime elision rules are so clean and nice that named lifetimes can largely be avoided (not async or anything, just floating point math). But, once a lifetime has to be named for a concrete type, everything that uses it also needs to name it. Traits and the magic for<'a> syntax seem promising, and I think my second option might lead to the least amount of work to wall off the lifetime-specified forms. I could probably also make separate owned and view (with lifetime) types, and give both of those a single trait interface to erase the lifetime at use points.

But what would be easiest is effectively the semantics of Index. An associated type Self::Type with no required lifetime, and a function that takes &self and gives &Self::Type with the same lifetime as self. Though in the case of ndarray it owns the data like a Vec, so a reference to a subset would be a slice &[f64], and then I'd have to say type Type = [f64]; which I see the issue of.

Index can support unsized types, but you still have to "contain" the returned type.

This is what it might look like with a GAT when you don't "contain" the returned type.

1 Like

Those give some interesting ideas. And this has made clear the constraint that's got me here. I'm using the newtype pattern, so lots of wrapping and implementations, including implementations for Copy types holding just an f64/f32. I've got a general aversion to adding too many type/lifetime parameters, but in this case blanket adding them to all types feels... inexact?

Also, moving the lifetime into the type makes Add<&'a Self, Output = Self> a double reference.

It's hard for me to get a feel for the shape of the code from prose. Speaking very generally, it can be verbose and sometimes tricky to abstract over borrow-or-owned, if that's the goal. If you do go that route, one strategy is to rely on specific lifetimes or lifetimes being equal as little as possible:

// This implementation "doesn't care" about specific lifetimes at all
impl<T> Mul<Borrowed<'_, T>> for Owned<T> {
    type Output = Self;
    fn mul(self, rhs: Borrowed<'_, T>) -> Self::Output { todo!() }
}

// Same here
impl<T> Mul<Borrowed<'_, T>> for Borrowed<'_, T> {
    type Output = Owned<T>;
    fn mul(self, rhs: Borrowed<'_, T>) -> Self::Output { todo!() }
}

// In contrast this requires the lifetimes to be the same and thus
// is less general than the one above
impl<T> Mul /* <Self> */ for Borrowed<'_, T> {
    type Output = Owned<T>;
    fn mul(self, rhs: Self) -> Self::Output { todo!() }
}

Covariance often makes things workable in concrete situations anyway, but the idea is to aim for maximum for<'any>-ness so that things work nicer in generic situations. You may still have a lot of gnarly for<'any> bounds.

There's a somewhat contrived example in the playground below.

1 Like

Yes! Abstracting over a borrowed-or-owned value is the target, and I guess I was looking to just use function elision to always keep things clean. Nice that the trait definition can use the anonymous lifetime. And I've at least been using all combinations of the fully qualified ops to implement things, thanks to &a + &b * &c vs. &a + &(&b * &c).

Also, I guess good to hear that I'm not really missing any options to simplify the process. Thanks for bearing with the prose version. Though I suppose this does at least highlight the question, if not the full stack of issues surrounding final use:

struct A<'a>(&'a f64);

trait GivesTypes {
    type Owned;
    type View;
}

struct B;

impl GivesTypes for B {
    type Owned = [f64; 3];
    
    // This is where I don't want to pick up a lifetime
    type View = A<'a> where Self::Owned: 'a;
}

struct VectorBase<B: GivesTypes>(pub B::Owned);

impl Index<usize> for VectorBase<B> {
    type Output = B::View;

    // Because I want to introduce it here, elided
    fn index(&self, index: usize) -> &Self::Output { ... }
}

And this is where an ArrayView2<'a, A> causes a problem:

trait VectorMath<T: FloatTraits>
where
    for<'o> Self: Add<T, Output = Self>
        + Sub<T, Output = Self>
        + Mul<T, Output = Self>
        + Div<T, Output = Self>
        + Add<Self, Output = Self>
        + Sub<Self, Output = Self>
        + Mul<Self, Output = Self>
        + Div<Self, Output = Self>
        + Add<&'o Self, Output = Self>
        + Sub<&'o Self, Output = Self>
        + Mul<&'o Self, Output = Self>
        + Div<&'o Self, Output = Self>,
    for<'s, 'o> &'s Self: Add<T, Output = Self>
        + Sub<T, Output = Self>
        + Mul<T, Output = Self>
        + Div<T, Output = Self>
        + Add<Self, Output = Self>
        + Sub<Self, Output = Self>
        + Mul<Self, Output = Self>
        + Div<Self, Output = Self>
        + Add<&'o Self, Output = Self>
        + Sub<&'o Self, Output = Self>
        + Mul<&'o Self, Output = Self>
        + Div<&'o Self, Output = Self>,
{}

The implications of this finally clicked. This can give me exactly what I want, a typed trait interface with no added lifetime that I can use for arguments by impl Trait<T>, perfect!! Obligatory playground link.

use ::core::ops::Add;

struct A<'a, T>(&'a T);

trait NoLifetime<T> {}

impl Add<f64> for A<'_, f64> {
    type Output = f64;
    
    fn add(self, rhs: f64) -> Self::Output {
        self.0 + rhs
    }
}

impl NoLifetime<f64> for A<'_, f64> {}

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.