How to Create `Fn` Bound That Includes Arguments With Higher-ranked Lifetime Bound

Here's an example of code that I want to work.

struct StaticSystem<In, Out> {
    run: &'static (dyn Fn(In) -> Out + Sync + Send),
}

fn example(_input: &str) {}

fn main() {
    let system = StaticSystem {
        run: &example,
    };
    
    let test = &String::from("test");
    (system.run)(test);
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0310]: the parameter type `In` may not live long enough
 --> src/main.rs:2:10
  |
2 |     run: &'static (dyn Fn(In) -> Out + Sync + Send),
  |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...so that the reference type `&'static (dyn Fn(In) -> Out + Send + Sync + 'static)` does not outlive the data it points at
  |
help: consider adding an explicit lifetime bound...
  |
1 | struct StaticSystem<In: 'static, Out> {
  |                       +++++++++

error[E0310]: the parameter type `Out` may not live long enough
 --> src/main.rs:2:10
  |
2 |     run: &'static (dyn Fn(In) -> Out + Sync + Send),
  |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...so that the reference type `&'static (dyn Fn(In) -> Out + Send + Sync + 'static)` does not outlive the data it points at
  |
help: consider adding an explicit lifetime bound...
  |
1 | struct StaticSystem<In, Out: 'static> {
  |                            +++++++++

For more information about this error, try `rustc --explain E0310`.
error: could not compile `playground` (bin "playground") due to 2 previous errors

The issue is I can't figure out how to have something like:

&'static (dyn for<'a> Fn(In: 'a) -> Out: 'a + Sync + Send)

Is there a way to restrict the lifetime of In and Out to be for<'a>?

I don't believe this is possible to express syntactically (except for literal reference types), but even if it were, well-formedness check doesn't seem to like it (hence the suggestion to add the 'static bound).

Yeah, I think I got around the syntax limitation with a helper trait, but I get the same error, so I think you're right:

struct StaticSystem<In, Out> {
    run: &'static (dyn for<'a> StaticSystemRunFn<'a, In, Out>),
}
trait StaticSystemRunFn<'a, In: 'a, Out: 'a>: Sync + Send {
    fn run(&self, input: In) -> Out;
}
impl<'a, T, In, Out> StaticSystemRunFn<'a, In, Out> for T
where
    In: 'a,
    Out: 'a,
    T: Fn(In) -> Out + Sync + Send,
{
    fn run(&self, input: In) -> Out {
        (self)(input)
    }
}

It still suggests adding the 'static bound.


Maybe I need to come at this question from another angle. This works:

struct StaticSystem<In, Out> {
    run: fn(In) -> Out,
}
fn example(_input: &str) {}

fn main() {
    let system = StaticSystem {
        run: example,
    };
    let test = &String::from("test");
    (system.run)(test);
}

Interestingly the example below doesn't work though. Shouldn't a static reference to the fn pointer be just as fine as the fn pointer itself?

struct StaticSystem<In, Out> {
    run: &'static fn(In) -> Out,
}
fn example(_input: &str) {}

fn main() {
    let system = StaticSystem {
        run: example,
    };
    let test = &String::from("test");
    (system.run)(test);
}

In my actual use-case, I'm needing to use a dyn Fn because the function I have is created from a closure that captures it's environment, so it can't be coerced into a function pointer.

Using a Box<dyn Fn> works well enough, but it allocates every time the system is requested, and I'm trying to figure out if I can manage to create a static reference to my closure with Box::leak() so that I can get a &'static dyn Fn that I can get multiple times without allocating each time.

Now that I'm walking through it it isn't seeming as important to avoid the Box<dyn Fn> since I'm not actually re-creating it throughout my app. But I'm still curious why &'static fn has extra lifetime bounds on the argument of the fn.

So, let's say you have a F: for<'f> Fn(&'f str). What happens when you pass it to this?

fn a<G: Fn(In), In>(_: G) {}

The answer is that the compiler has to choose a single type to resolve the In type parameter. So it chooses a single lifetime, 'static say, and you can pass in your F: Fn(&'static str). It's no longer higher-ranked, because generic type parameters must resolve to a single type (and types that differ by lifetime only are still distinct types).


There is a way partially around this with custom traits, but it's a pain and wrecks a lot of inference. The key trick is to avoid "naming" higher-ranked things with type parameters. (More details.)

But I don't think it helps with this type-erased form, because you basically want to end up with something like

trait BorrowingFn {
    type In<'a>;
    type Out<'a>;
    fn call<'a>(&self, input: Self::In<'a>) -> Self::Out<'a>;
}

let _ = &dyn BorrowingFn< ... ? ... >

and we don't have a way to support or even specify GATs/higher-ranked things in trait objects yet.

// Imaginary syntax for some future Rust
dyn BorrowingFn<for<'x, 'y> In<'x> = &'x String, Out<'y> = &'y str>>

As for the 'static bounds, a fn(&'a str): 'static bound fails for example; the lifetime relationship is syntactical in nature. It's probably the same thing going on with Fn, but I also think it's not your actual problem and just a symptom, so I won't dig any further.

(fn(&str): 'static is a bound that can be met.)

2 Likes

Hmm, no, there is a way, you "just" need non-GAT representatives of all of your type constructors.

Probably too much boiler plate for every type of input-output type constructors you were thinking of though?


I assume you mean capturing by move. Otherwise you can't meet a 'static bound. Even then you can't have a &'static CapturingClosure without leaking the closure.

Leaking example.

1 Like

Yes, that's what I was thinking.

Oh, that's interesting. Thanks for all of the examples!

At this point, yeah, the juice really isn't worth the squeeze, but I still appreciate the help for sure. :+1:

1 Like

Of course I didn't step back and think about it until after I made the example, but there's no real point in leaking either when you can just store a Box<dyn ...> instead, heh.

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.