Removing the lifetime from this function signature

Hello fellow Rustaceans :wave:

I'm trying to make a function generic over both sqlx Transaction and PoolConnection. Unfortunately, the Executor trait requires a generic lifetime parameter.

use anyhow::Result;
use std::time::Duration;
use sqlx::{self, postgres::PgPoolOptions, Executor, Pool, Postgres, PgExecutor, Transaction};

pub type DB = Pool<Postgres>;
pub trait Queryer<'c>: Executor<'c, Database = sqlx::Postgres> {}

impl<'c> Queryer<'c> for &Pool<Postgres> {}
impl<'c> Queryer<'c> for &'c mut Transaction<'_, Postgres> {}

async fn do_something_generic(
    db: impl Queryer<'_>,
    param: String,
) -> Result<()> {
    return Ok(());
}

#[tokio:main]
async fn main() -> Result<()> {
   let db = PgPoolOptions::new()
        .connect(&database.url)
        .await?;

    let mut tx = db.begin().await?;

    do_something_generic(&mut tx, "hello".to_string()).await?;

    return Ok(());
}

Any idea how I could remove the <'_> from the do_something_generic function, whether it be a type alias, a new trait or a complete refactoring?

I find it very ugly.

Any help is appreciated :slight_smile:

A single generic lifetime parameter is not "ugly". If you need it, you need it.

3 Likes

Actually I'm not sure it's needed because the transaction / database pool will never outlive the function.

In the current state of affairs, it's only required because of the sqlx::Executor trait.

I think I'm about to succeed but I still have an error:

pub trait Queryer: for<'a> PgExecutor<'a> {}
impl<'c> Queryer for &Pool<Postgres> {}
impl<'c, 't> Queryer for &'t mut Transaction<'c, Postgres>{}

async fn do_something_generic(
    db: impl Queryer,
    param: String,
) -> Result<()> {
    return Ok(());
}
error: implementation of `Executor` is not general enough
  --> src/main.rs:26:27
   |
26 | impl<'c, 't> Queryer5 for &'t mut Transaction<'c, Postgres>{}
   |                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `Executor` is not general enough
   |
   = note: `Executor<'0>` would have to be implemented for the type `&'t mut Transaction<'c, Postgres>`, for some specific lifetime `'0`...
   = note: ...but `Executor<'_>` is actually implemented for the type `&mut Transaction<'_, Postgres>`

Any idea?

for<'any> Trait<'any> is an entirely different bound than Trait<'_>. The former says "the type implements the trait for all lifetimes", while the latter says "the type implements the trait for a single caller-chosen lifetime".

And concrete lifetimes are part of types. That is, &'x Foo and &'y Foo are only the same type if 'x and 'y are the same lifetime, even though variance can make it seem otherwise.

So here:

impl<'c> Queryer<'c> for &Pool<Postgres> {}

&'x Pool<Postgres>: for<'any> Queryer<'any> for all 'x, but here:

impl<'c> Queryer<'c> for &'c mut Transaction<'_, Postgres> {}

&'c mut Transaction<'_, Postgres>: Queryer<'c> and does not meet the bound for any other lifetime. That is, &'c mut Transaction<'_, Postgres>: Queryer<'d> is not true, and certainly &'c mut Transaction<'_, Postgres>: for<'any> Queryer<'any> is not true.


The <'_> here is conveying relevant information: the trait is lifetime-specific, but the function can work with any lifetime the caller chooses. Therefore the caller is free to pass in a type that only implements the trait for a single lifetime (like &'c mut Transaction<'_, Postgres>). The <'_> represents the function being parameterized by that caller-chosen lifetime.

There's no way to remove the <'_> without also removing that lifetime parameter, which makes your function unable to receive the same set of types. For example your attempt restricts the function to accepting types that implement the trait for all lifetimes, like &Pool<Postgres>.


Something that's even uglier than seeing <'_> is debugging lifetime errors involving the places where Rust does let you omit them entirely, which is why the elided_lifetimes_in_paths lint exists, and why newer features like return-position impl Trait require '_ when they capture lifetimes. Invisible borrowing causes confusion and consternation.

The return of an async fn captures its input lifetimes. If you had the ability to get rid of <'_> here, there would no longer be an indication that the returned Future was limited by the lifetime of the inputs.

6 Likes

Wow, thank you very much!

It's clearer now, my understanding of the what the code was saying was not clear.

Still, I find this lifetime parameter inelegant and distracting, whether it be async fn do_something_generic<'a>(db: impl Queryer<'a>) or async fn do_something_generic(db: impl Queryer<'_>).

Any idea how I could 'hide' it? Maybe with types aliases, a trait with an associated type or with a specific trait? So far I've been unsuccessful.

I don't know of any way to hide this case in the source code without changing the meaning of the code. As far as I know it doesn't exist. Perhaps there's an IDE that hides them.

(If there was a way to hide it, your code would be more frustrating for everyone else experienced enough to understand a lifetime indicates borrowing. For this reason, I doubt any request to allow elision here would be entertained.)


When we get async fn in traits, you could move the lifetime elsewhere (to the impl header).

pub trait DoSomething: Sized {
    async fn do_something_generic(self, param: String) -> Result<()>;
}

impl<'x, Db: Queryer<'x>> DoSomething for Db {
    async fn do_something_generic(self, param: String) -> Result<()> {
        Ok(())
    }
}

Thank you very much for your time and kind explanations!

I think I will have to abdicate until maybe one day the upstream library updates its interface and this <'_>.

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.