Differences with lifetime bounds between associated and free functions

Came across this issue today while trying to work with function pointers.

Consider an arbitrary struct with a lifetime parameter: struct Stackable<'gc> { ... }

I have a function pointer type defined like this: type NativeFunction = for<'gc> fn(Stackable<'gc>) -> Result<Option<Stackable<'gc>>, Error>. (As far as I understand it this is both the correct and only way of using the struct in a function pointer type, specifying that for any given lifetime, the function will consume a value and produce one with the same lifetime.)

The following free function conforms to this function pointer type: fn do_something<'gc>(value: Stackable<'gc>) -> Result<Option<Stackable<'gc>>, Error> { ... }

But an associated function on Stackable does not, even though it appears to have identical type:

impl<'gc> Stackable<'gc> {
    fn do_something(self) -> Result<Option<Stackable<'gc>>, Error> {
        todo!()
    }
}
error[E0308]: mismatched types
expected fn pointer `for<'gc> fn(Stackable<'gc>) -> Result<Option<Stackable<'gc>>, _>`
   found fn item `fn(Stackable<'_>) -> Result<Option<Stackable<'_>>, _> {Stackable::<'_>::do_something}`

I can’t seem to understand the (variance?) issue here. To me it would appear that while the type dictates that the function needs to be callable for any given lifetime, the associated function is callable for any lifetime where the output lifetime matches the self lifetime, which in turn can be any given lifetime. Those two things are just two sides of the same coin to me.

Are there differences between lifetime parameters on types and functions?

Playground link

I am not anywhere close to a Rust type system expert so I can't give you the full story. But I believe with a method, the lifetime of Self cannot be determined by the caller of the method. Rather it is determined by the creator of Self.

Therefore, a non-method function with an explicit lifetime is the only way to allow choosing that lifetime in the caller:

    fn do_something<'gc2>(this: Stackable<'gc2>) -> Result<Option<Stackable<'gc2>>, ()> {

I am not a Rust expert either, but I think it has something to do with early bound and late bound parameters.

With free function you can specify lifetime when calling the function, but with associated function you have to specify lifetime when naming the function and after you do that it cannot satisfy the signature of your NativeFunction type.

I.e. you have to say Stackable<'gc>::do_something fixing the lifetime. Even if you say Stackable::do_something, it is just abbreviation of the former with the lifetime inferred.

As far as I know, there is no way to name the associated function while leaving lifetime free.

(You can always wrap it in a free function like

fn do_something_free<'gc>(s: Stackable<'gc>) -> Result<Option<Stackable<'gc>>, Error> {
    s.do_something()
}

but I guess you already knew it.)

4 Likes

This small modification gives the same error, and might help illustrate the issue:

 fn main() {
+    let a = String::from("hey");
+    provided_lifetime(&a);
+}
+
+fn provided_lifetime<'a>(_: &'a str) {
     // ERROR: doesn’t compile
-    accept_native(Stackable::do_something);
+    accept_native(<Stackable<'a>>::do_something);
     accept_native(do_something_freely);
+}

That is, accept_native isn't allowed to call that function with multiple different lifetimes in the first call, since one lifetime for <Stackable<'gc>>::do_something must be chosen to pick the function definition (the function item itself does not have any generic lifetimes attached).
At least, this is my intuition.

1 Like

As mentioned by @Tom47, this is due to early vs. late binding of lifetimes. I recommend reading this article from Niko. The gist is that since lifetimes are erased at runtime, the compiler doesn't monomorphize functions based on them; in contrast to "normal" types where it does monomorphize them since the type affects size, alignment, etc. This means that function pointers must not have any type/(early-bound) lifetime parameters. In your case, there are early-bound lifetimes involved, 'gc, preventing it from being used as function pointer.

An interesting consequence of that is even a free function like below cannot be made into a function pointer due to the existence of a bound which forces it to be "early bound":

fn main() {
    // Uncommenting below will cause a compilation falure since
    // the lifetime parameter `'a` in `foo` is actually early
    // bound.
    // let f: for<'a> fn(&'a str) = foo;
}
fn foo<'a: 'a>(x: &'a str) {
}

One possible workaround besides defining a free function is using traits like Fn instead. For example instead of using fn pointers, rely on a type parameter, F, with a bound like 'gc, F: Fn(Stackable<'gc>) -> Result<Option<Stackable<'gc>>, Error>.

3 Likes

I’m fully aware that both lifetimes have to match up. It’s just that based on the type signature the compiler outputs (and what I specify), I cannot see the difference between one matching pair of lifetimes and the other, when after all there clearly is one.

Thanks for the workaround hint; since I need to store the function pointers permanently and require some performance when retrieving and calling the functions through the pointers, I don’t want to use the traits and their more expensive (?) dynamic dispatch.

By the way, does this qualify for a UI bug? As in, the compiler should tell me about how and why the lifetimes are different.

Parts of this reply may be redundant with others being posted as I wrote it.


The problem is that the binder -- the for<..> in for<'gc> fn(...) -- doesn't line up with where the generics are on Stackable::<'_>::do_something. The way things work are sort of as if you had

fn do_something_freely<'gc>(_v: Stackable<'gc>) -> Result<Option<Stackable<'gc>>, ()> {
    Err(())
}

// No parameter type
struct DoSomethingFreely;
// Implemented for all lifetimes with no constraints
impl<'gc> Fn(Stackable<'gc>) -> Result<Option<Stackable<'gc>>, ()> 
    for DoSomethingFreely
{ ... }

// ------------ Versus  ------------

impl<'gc> Stackable<'gc> {
    // A function item type that takes `Stackable<'gc>` only
    fn do_something(self) -> Result<Option<Stackable<'gc>>, ()> {
        Err(())
    }
}

// Parameterized by the lifetime
// (`StackableDoSomething<'a>` and `StackableDoSomething<'b>` are different
//  types unless `'a` and `'b` are the exact same lifetime)
struct StackableDoSomething<'gc>(..);
// Each type implements for one specific lifetime
impl<'gc> Fn(Stackable<'gc>) -> Result<Option<Stackable<'gc>>, ()> 
    for StackableDoSomething<'gc>
{ ... }

For the do_something_freely function, that is what is called a late bound parameter, meaning it appears in the trait parameters but not in the implementing type parameters:

impl<'gc> Fn(Stackable<'gc>) -> Result<Option<Stackable<'gc>>, ()> 
    for DoSomethingFreely

And that is what allows implementations to meet a higher-ranked bound:

for<'gc> Implementor: Fn(Stackable<'gc>) -> Result<Option<Stackable<'gc>>, ()>

Which is required for casting to the analogous function pointer here.

And a parameter is called early bound if it appears in the implementing type. Here, it's not really even on the implementing type (on the function item) itself, but it's part of the definition of the function item: Stackable::<'gc>::do_something.


You don't have to define a free-standing function to work around this, you can just introduce a closure.

    accept_native(|x| Stackable::do_something(x));

Function pointers are similar to dynamic dispatch in terms of being an indirection invocation, though they won't require allocation like a Box<dyn ...> may.[1]

Using a generic F: Fn(..) would avoid those indirections. Generics do static dispatch. dyn Trait is what does dynamic dispatch.

That being said, if you need multiple function pointers, you probably need type erasure, and you're right back at needing function pointers or dyn Trait.

The diagnostics should suggest the closure workaround IMO. There are probably a lot of related issues already open though...


  1. If the erased type is zero sized -- like all the examples in this topic have been -- there will be no allocation and it will probably be similar to using a function pointer. ↩︎

4 Likes

I'd be quite surprised if there were any measurable performance difference. There is no dynamic dispatch involved in what I suggested unless you rely on dyn Fn.

I didn’t express myself clearly enough then. I would definitely need dynamic dispatch, as I have a large collection of functions with the same call signature stored in a table. I don’t need the generic type (or the boxing + extra allocation) since I don’t need to use closures at all, plain functions are enough.

Another option here would have been to write:

impl Stackable<'_> {
    fn do_something<'gc>(this: Stackable<'gc>) -> Result<Option<Stackable<'gc>>, Error> {
        todo!()
    }
}

but you do lose the method sugar by doing this (but hopefully it helps illustrate the difference between the two a bit better! Indeed, compare it to

impl<'gc> Stackable<'gc> {
    fn do_something(this: Stackable<'gc>) -> Result<Option<Stackable<'gc>>, Error> {
        todo!()
    }
}

)

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.