Fn trait with trait argument with lifetime

Hi everybody,

After some testing, I realized that:

pub trait MyTrait<'b> {
    fn doit() -> ();
}

fn runit<'a, T, F, Z>(a: &'a str, b: &'a str, f: F) -> T
where
    for<'b> F: Fn(&'b str, &'b str, Z) -> T,
    for<'b> Z: MyTrait<'b>
{
    //...
}

doesn't actually mean

  /////////////////////////////// vvvvvvv //////////// won't compile
  for<'b> F: Fn(&'b str, &'b str, Z<'b>) -> T,

I wonder, is there syntax for that or is the idea to

fn runit<'a, 'b, T, F, Z>

without using for keyword?

Thank you.

Right, type variables like Z represent a single type, and can't represent a lifetime-parameterized type constructor.

There is no syntax for a "generic type constructor bound".[1] Sometimes you can fake it with supertrait bounds. Being generic over, say, "1-or-0 parameterized types/type constructors" is somewhat possible, but generally takes some boilerplate like a custom trait, and it's tricky to not completely wreck inference.

You can go down the road of GATs, but they're pretty limiting when it comes to higher-ranked bounds in some ways. You can go down this route if you run into those limits, but it has its own tradeoff.

If you have a more concrete example I may attempt a more concrete response.


  1. or for<'b> exists<Z> ↩ī¸Ž

2 Likes

Thank you @quinedot

I tried to simplify a larger codebase into a smaller example.

The code worked fine until I had to add lifetimes to foo and bar functions. Then I started having issues with the combination of fn and Fn.

const ALLFUNC: [SomeDatFunc; 2] = [
    // foo
    SomeData::foo,
    // bar
    SomeData::bar,
];

pub type SomeDatFunc<'a> = fn(&'a str, &'a str) -> Option<SomeData<'a>>;

pub trait TraitSomeData<'a> {
    fn whatever(&'a self) -> &'a str;
}

pub trait TraitFinal<'a> {
    fn doit<F, X>(f: F, first: &'a str, second: &'a str) -> Option<X>
    where
        for<'x> F: Fn(&'x str, &'x str) -> Option<X>,
        for<'x> X: TraitSomeData<'x>;
}

pub struct SomeData<'a> {
    pub data: &'a str,
}
impl<'a> SomeData<'a> {
    pub fn foo<'x>(first: &'x str, second: &'x str) -> Option<SomeData<'a>>
    where
        'x: 'a,
    {
        Some(SomeData { data: first })
    }
    pub fn bar<'x>(first: &'x str, second: &'x str) -> Option<SomeData<'a>>
    where
        'x: 'a,
    {
        Some(SomeData { data: second })
    }
}
impl<'a> TraitSomeData<'a> for SomeData<'a> {
    fn whatever(&'a self) -> &'a str {
        self.data
    }
}

struct Final<'a> {
    data: SomeData<'a>,
}
impl<'a> TraitFinal<'a> for Final<'a> {
    fn doit<F, T>(f: F, first: &'a str, second: &'a str) -> Option<T>
    where
        for<'x> F: Fn(&'x str, &'x str) -> Option<T>,
        for<'x> T: TraitSomeData<'x>,
    {
        (f)(first, second)
    }
}

fn main() {
    let a = ALLFUNC.get(0).unwrap();
    // implementation of `Fn` is not general enough
    let x = <Final as TraitFinal>::doit(a, "hi", "hello");
    let y = x.unwrap().whatever();
}
1 Like

It might be easier to use a function pointer (fn) over trait (Fn):

const ALLFUNC: [SomeDatFunc; 2] = [
    // foo
    SomeData::foo,
    // bar
    SomeData::bar,
];

pub type SomeDatFunc<'a> = fn(&'a str, &'a str) -> Option<SomeData<'a>>;
pub type SomeDatFuncGeneric<'a, T> = fn(&'a str, &'a str) -> Option<T>;

pub trait TraitSomeData<'a> {
    fn whatever(&'a self) -> &'a str;
}

pub trait TraitFinal<'a> {
    fn doit<T>(f: SomeDatFuncGeneric<'a, T>, first: &'a str, second: &'a str) -> Option<T>
    where
        T: TraitSomeData<'a>;
}

pub struct SomeData<'a> {
    pub data: &'a str,
}
impl<'a> SomeData<'a> {
    pub fn foo<'x>(first: &'x str, second: &'x str) -> Option<SomeData<'a>>
    where
        'x: 'a,
    {
        Some(SomeData { data: first })
    }
    pub fn bar<'x>(first: &'x str, second: &'x str) -> Option<SomeData<'a>>
    where
        'x: 'a,
    {
        Some(SomeData { data: second })
    }
}
impl<'a> TraitSomeData<'a> for SomeData<'a> {
    fn whatever(&'a self) -> &'a str {
        self.data
    }
}

struct Final<'a> {
    data: SomeData<'a>,
}
impl<'a> TraitFinal<'a> for Final<'a> {
    fn doit<T>(f: SomeDatFuncGeneric<'a, T>, first: &'a str, second: &'a str) -> Option<T>
    where
        T: TraitSomeData<'a>,
    {
        (f)(first, second)
    }
}

fn main() {
    let a = ALLFUNC.get(0).unwrap();
    let x = Final::doit(a.clone(), "hi", "hello");
    let y = x.unwrap().whatever();
}

I poked at it enough to get it working. Do you control all these traits and types by the way?

Some things I noted upfront:

  • You have elided lifetimes in ALLFUNC. Here's the expanded definition:

    #![deny(elided_lifetimes_in_paths)] // helps to find these types of errors
    const ALLFUNC: [SomeDatFunc<'static>; 2] = [
        SomeData::foo,
        SomeData::bar,
    ];
    
  • Maybe you meant to have a higher-ranked type here instead...

    pub type SomeDatFunc = for<'a> fn(&'a str, &'a str) -> Option<SomeData<'a>>;
    

    ...in which case you need an adjustment elsewhere:

    -pub fn foo<'x>(first: &'x str, second: &'x str) -> Option<SomeData<'a>>
    -where
    -    'x: 'a,
    -{
    +pub fn foo<'x>(first: &'x str, second: &'x str) -> Option<SomeData<'x>> {
    

    (And similarly for bar.) The reason are those HRTB limitations I mentioned. Note that being more general with the lifetimes didn't help you from a practical point of view: callers can coerce to the shorter lifetime at the call site with the new signature -- and they were doing so already in practice, because no one wants to borrow something longer than required.

  • This implementation can be more general:

    -impl<'a> TraitSomeData<'a> for SomeData<'a> {
    -    fn whatever(&'a self) -> &'a str {
    +impl<'a> TraitSomeData<'_> for SomeData<'a> {
    +    fn whatever(&self) -> &'a str {
             self.data
         }
    }
    

    You're just copying the shared reference self.data out. You don't need to force yourself to be remain borrowed for 'a. This was also a required change for the fix, and it's also part of why I asked if you controlled all the traits. It feels like there may be some mismatch between the traits and what is actually being implemented. That said, I stopped trying to figure out the overall design once I got things to work.

    Before this change you were taking a self: &'a SomeData<'a>, which is sometimes okay since we're dealing with shared references here, but it's still a yellow flag. It was problematic in this case.


With those out of the way, we can address the topic at hand. The problem is that the Fn trait sugar has become salt by forcing you to name the (potentially borrowing) output type. But when you're being generic, the only way you have to name the output is a generic type variable -- which can't represent more than one single type.

For the playground, it's sufficient to use a subtrait to avoid having to name the return type.

pub trait Call<'x>: Fn(&'x str, &'x str) -> Option<Self::Out> {
    type Out;
}

impl<'x, F: Fn(&'x str, &'x str) -> Option<Out>, Out> Call<'x> for F {
    type Out = Out;
}

Note how we parameterized the trait on the lifetime -- so here the Out can still be borrowing (be a type that mentions 'x), because we're only dealing with one lifetime, not all lifetimes yet.

With this trait, your bounds will look like this:

-    fn doit<F, X>(f: F, first: &'a str, second: &'a str) -> Option<X>
-    where
-        for<'x> F: Fn(&'x str, &'x str) -> Option<X>,
-        for<'x> X: TraitSomeData<'x>;
+    fn doit<F>(f: F, first: &'a str, second: &'a str) -> Option<<F as Call<'a>>::Out>
+    where
+        for<'x> F: Call<'x, Out: TraitSomeData<'x>>;

So you'll have to be able to change the bounds on the traits. If you want to make them simpler, you could move the Out: TraitSomeData<'x> bound to the Call trait's associated type.

With all the changes above, the playground compiles.

1 Like

Thank you very much @quinedot

Yes, I do.

Useful!

I didn't realize that.

This is superb!

Thank you. I simplified it a lot and it compiles now!

Another thing I realized is that I have some lifetime problem that I introduced by being explicit (and wrong in some place). I realized that when I remove a lot of lifetimes, the program compiles.

1 Like