Troubles with higher ranked lifetime projections and GAT

I'm looking for input to some complicated lifetime setup. I've build a minimal example here: Rust Playground

(Please ignore the wired back and forth between spawn_blocking and block_on, that's mostly there to get the right bounds for the async code involved in the real code).

Now my question is why does this code not work? I would expect that the 'static bound for OwnedRow enforces that the return type lives long enough to be returned from that thread, given that the return type is essentially Vec<OwnedRow> which shouldn't be allowed to contain a non-static reference at all. Do I miss something obvious or is that a limitation in the compiler. Do you know of some possible workarounds to make this compile?

The 'static on OwnedRow does not do what you intend.
The types lifetime is the intersection of all lifetime specifiers.
OwnedRow has implicit lifetime of no greater than 'a

(not read further into code)

Thanks for your answer.
So the problematic part is that it references the 'conn lifetime as part of the earlier trait projection. I suppose there is no way to express the required constraint without removing the lifetimes there?

I think the problem is that even though the type is 'static, it does mention some lifetimes, which can cause problems if that type is used in a context where those lifetimes are invalid.

One way to solve this problem is to remove those lifetimes, for example by replacing them with 'static. This reduces the problem to casting Vec<<C::Row<'conn, 'query> as Row<'conn>>::OwnedRow> to Vec<<C::Row<'static, 'static> as Row<'static>>::OwnedRow>, however the compiler cannot see that they are the same type. To fix this you could do runtime casting: since the types are 'static you can cast references to them to dyn Any and then downcast it, which should never fail because under the hood the types must be the same. This feels very hacky but at least compiles

3 Likes

Thanks for providing that solution. At least that's something that compiles :heart:

If I understand correctly, in practice for the implementation, <C::Row<'conn, 'query> as Row<'conn>>::OwnedRow is the same type for any lifetimes 'conn, 'query. You can work this into the trait bounds on the implementation:

//      v new
impl<C, O> Connection for Wrapper<C>
where
    C: Connection + 'static,
    // new
    O: 'static + Send + for<'conn> Row<'conn>,
    // new
    for<'conn, 'query> C::Row<'conn, 'query>: Row<'conn, OwnedRow = O>,
    // the other HRTB is now redundant so I removed it
{
    // defined in terms of `O` which must be a single (and `'static`) type
    type Row<'conn, 'query> = O where Self: 'conn;

    // Same for the return type
    fn load<'conn, 'query, T>(&'conn mut self, query: T) 
        -> impl Iterator<Item = O>

This compiles, but I'm not sure how well the compiler will fare at "finding" the implementation when you try to use it, or if I've overlooked some other HR nuances that differ between the versions.

If your returned iterators are always nameable like the one in the implementation is, you could use a GAT instead of -> impl Iterator. Then you can pick which input generics ('conn, 'query, T) are able to be captured or not. If nothing ever needs captured, that would be a (non-generic) associated type.

2 Likes

It seem to have no problem with finding the right implementation for that: Rust Playground

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.