Can't figure out how to generalize function using generics

I have a with_user_repo and with_item_repo functions which are identical except for the type of Repository used within these functions. I would like to write a generic version, called with_repo, that is generic over Repository types. However, I keep getting a compile error which I don't understand how to fix.

Minimal complete commented reproducible example (playground link):

#![allow(unused_variables)]

// this type cannot be Clone or Copy
struct Connection;

trait Repository<'a> {
    fn new(conn: &'a Connection) -> Self;
}

// this struct cannot change
struct UserRepository<'a> {
    conn: &'a Connection,
}

// this struct cannot change
struct ItemRepository<'a> {
    conn: &'a Connection,
}

impl<'a> Repository<'a> for UserRepository<'a> {
    fn new(conn: &'a Connection) -> Self {
        Self { conn }
    }
}

impl<'a> Repository<'a> for ItemRepository<'a> {
    fn new(conn: &'a Connection) -> Self {
        Self { conn }
    }
}

// works
fn with_user_repo<F>(f: F)
where
    F: FnOnce(UserRepository<'_>, &Connection),
{
    let conn = Connection {};
    let user_repo = UserRepository::new(&conn);
    f(user_repo, &conn);
}

// works
fn with_item_repo<F>(f: F)
where
    F: FnOnce(ItemRepository<'_>, &Connection),
{
    let conn = Connection {};
    let item_repo = ItemRepository::new(&conn);
    f(item_repo, &conn);
}

// fails
fn with_repo<'a, R, F>(f: F)
where
    R: Repository<'a>,
    F: FnOnce(R, &Connection),
{
    let conn = Connection {};
    let repo = R::new(&conn);
    f(repo, &conn);
}

fn main() {
    // works
    with_user_repo(|user_repo, conn| {
        println!("with_user_repo works");
    });

    // works
    with_item_repo(|item_repo, conn| {
        println!("with_item_repo works");
    });

    // fails
    with_repo::<UserRepository, _>(|user_repo, conn| {
        println!("with_repo::<UserRepository, _> fails");
    });

    // fails
    with_repo::<ItemRepository, _>(|item_repo, conn| {
        println!("with_repo::<ItemRepository, _> fails");
    });
}

Throws:

error[E0597]: `conn` does not live long enough
  --> src/main.rs:59:23
   |
53 | fn with_repo<'a, R, F>(f: F)
   |              -- lifetime `'a` defined here
...
59 |     let repo = R::new(&conn);
   |                -------^^^^^-
   |                |      |
   |                |      borrowed value does not live long enough
   |                argument requires that `conn` is borrowed for `'a`
60 |     f(repo, &conn);
61 | }
   | - `conn` dropped here while still borrowed

If anyone could help me understand why the generic version does not work, and what I can do to make it work, I would greatly appreciate it. Thank you.

Sure! It's because generic parameters are chosen by the caller. If I call the function with_repo<'static, ItemRepository<'static>, _>, is your code then correct? The answer is no, because R::new takes an &'static Connection, but your connection doesn't live that long.

I don't think there is an easy fix. Normally with this kind of problem you would do this:

fn with_repo<R, F>(f: F)
where
    R: for<'a> Repository<'a>,
    F: FnOnce(R, &Connection),
{
    ...
}

This says that R implements the infinite list of traits Repository<'a>, Repository<'b>, Repository<'c> and so on for every possible lifetime.

However, this does not work when you have a constructor in the trait because if I call with_repo<ItemRepository<'long>, _>, then that type does not implement Repository<'short>, as would be required by the "every lifetime" requirement.

I would probably try to find a solution that either

  1. avoids the references entirely, or
  2. does not have any constructors in the trait.
1 Like

If that makes sense in your code, your can try to construct Connection outside of with_repo:

fn with_repo<'a, R, F>(conn: &'a Connection, f: F)
where
    R: Repository<'a>,
    F: FnOnce(R, &Connection),
{
    let repo = R::new(conn);
    f(repo, &conn);
}

fn main() {
    let conn = Connection;
    with_repo::<UserRepository, _>(&conn, |user_repo, conn| {
        println!("with_repo::<UserRepository, _> works");
    });

    let conn = Connection;
    with_repo::<ItemRepository, _>(&conn, |item_repo, conn| {
        println!("with_repo::<ItemRepository, _> works");
    });
}
2 Likes