Changing member structure with lifetime

Hello,

I'm trying to figure out how to do something like this (it's mostly the idea):

use std::ops::Fn;

#[derive(Default)]
struct SomeStructValue {
    pub dummy: i32
}

pub type MyFn<'a, T> = Box<dyn Fn(&T) -> i32 + 'a>;

struct MyStruct<'a> {
    struct_value: SomeStructValue,
    my_fn: MyFn<'a, i32>
}

impl<'a> MyStruct<'a> {
    pub fn new(struct_value: SomeStructValue) -> Self {
        let mut my_struct = MyStruct {
            struct_value,
            my_fn: Box::new(|_: &i32| 0i32)
        };
        
        my_struct.my_fn = Box::new(|_: &i32| {
            let value = &my_struct.struct_value;
            value.dummy
        });
        
        my_struct
    }
}

But the compiler (obviously) tells me:

error[E0506]: cannot assign to `my_struct.my_fn` because it is borrowed
  --> src/lib.rs:22:9
   |
15 | impl<'a> MyStruct<'a> {
   |      -- lifetime `'a` defined here
...
22 |         my_struct.my_fn = Box::new(|_: &i32| {
   |         ^^^^^^^^^^^^^^^            --------- `my_struct.my_fn` is borrowed here
   |         |
   |         `my_struct.my_fn` is assigned to here but it was already borrowed
23 |             let value = &my_struct.struct_value;
   |                          ---------------------- borrow occurs due to use in closure
...
27 |         my_struct
   |         --------- returning this value requires that `my_struct` is borrowed for `'a`

error[E0373]: closure may outlive the current function, but it borrows `my_struct`, which is owned by the current function
  --> src/lib.rs:22:36
   |
22 |         my_struct.my_fn = Box::new(|_: &i32| {
   |                                    ^^^^^^^^^ may outlive borrowed value `my_struct`
23 |             let value = &my_struct.struct_value;
   |                          ---------------------- `my_struct` is borrowed here
   |
note: closure is returned here
  --> src/lib.rs:27:9
   |
27 |         my_struct
   |         ^^^^^^^^^
help: to force the closure to take ownership of `my_struct` (and any other referenced variables), use the `move` keyword
   |
22 |         my_struct.my_fn = Box::new(move |_: &i32| {
   |                                    ++++

Obviously there is a problem with the lifetime since I'm re-affecting the borrowed member to a new Box.

I am trying to have a Fn (not MutFn or FnOnce) so it means (if I'm not wrong) that all values should only be borrowed. But since the parameter passed to MyStruct::new is moved (not borrowed). It means I need to move the argument into MyStruct in order to be able to reference it in the Fn. So I can only initialize my_fn once I have a concrete address that I can reference for struct_value.

Hoping I was clear enough, I am searching for a solution to this problem.

Thank you very much in advance for any help

That's a self-referencial struct. You can't move one after you create it (the reference would dangle[1]). There's no safe way to make a useful one (using actual references).


  1. and that's not the only problem ↩︎

Thank you for your time.

So from what I understand, it seems impossible ?

In it's current form it's impossible, yes.

I'm not clear on what you're actually trying accomplish so I can't recommend any alternatives.

To be I'm trying to store a StyleFn in my structure. But the StyleFn takes only one parameter and I need to access further information (passed in the ::new(...) function).

Thank you for your help

If the SomeStructValue does not change over the lifetime of MyStruct, then what you are looking for is shared ownership:

use std::ops::Fn;
use std::sync::Arc;

#[derive(Default)]
struct SomeStructValue {
    pub dummy: i32
}

pub type MyFn<'a, T> = Box<dyn Fn(&T) -> i32 + 'a>;

struct MyStruct {
    struct_value: Arc<SomeStructValue>,
    my_fn: MyFn<'static, i32>
}

impl MyStruct {
    pub fn new(struct_value: Arc<SomeStructValue>) -> Self {
        MyStruct {
            struct_value: struct_value.clone(), // clones the Arc pointer only
            my_fn: Box::new(move |_: &i32| {
                struct_value.dummy
            })
        }
    }
}

Another possibility is to put the needed data into the T (Theme in the docs you linked) parameter. It will help if you tell us more information about your use case — where is the data coming from? When, if at all, does it change? Why are you storing a StyleFn in particular in your structure, and writing the function too, but still required to use iced's function signature?

1 Like

Thank you for your help.

By looking at your answer I think I'm a bit confused about Fn, FnOnce and FnMut.

I looked at this stackoverflow question and from this answer and I understood that:

Fn (&self) are functions that can be called if they only have & access to their environment

Si I thought that an Fn couldn't move any surrounding environment and the rust doc says:

Fn is implemented automatically by closures which only take immutable references to captured variables or don’t capture anything at all

But your example shows a move which seems contradictory (I'm just trying to understand).

So how is your move "compatible" with a Fn ?

where is the data coming from?

It's coming from a deserialized json

When, if at all, does it change?

For the moment, never. It's only parsed once. But it could change in the future (if one needs to take into account the changes made to the file while running)

Why are you storing a StyleFn in particular in your structure, and writing the function too, but still required to use iced's function signature?

Indeed, it seems kind of stupid and useless. Storing it is completely useless.

Thank you for your help

Which Fn traits get implemented depends on how a closure's captures are used, not directly on what they are. That phrasing could be improved, but they don't mean that the closure can only capture shared references. They mean that it only uses what is captured in a way compatible with shared references.

Think of a closure as a compiler-generated struct that has its captures as fields. It implements some number of the Fn traits based on how calling the closure needs to manipulate those fields. Just like you can have a method that takes &self on a struct that owns a String, you can have a closure that captures a String by value and implements Fn.

Or in the code you were asking about, a closure that captures an Arc<SomeStructValue>.

1 Like

Thank you for your explanation and time.

Which Fn traits get implemented depends on how a closure's captures are used, not directly on what they are.

I hope my question makes sense, but how does the compiler know if the closure is called only once or multiple times ? (I hope it's not a too stupid question)

They mean that it only uses what is captured in a way compatible with shared references.

How does the compiler know it's a shared reference ? Arc (for example) is not "explicitly" (for the compiler I mean) stating it's a shared reference. (I hope I'm clear enough)

Think of a closure as a compiler-generated struct that has its captures as fields. It implements some number of the Fn traits based on how calling the closure needs to manipulate those fields. Just like you can have a method that takes &self on a struct that owns a String, you can have a closure that captures a String by value and implements Fn.

Great I understand much better

Thank you again for your explanation and time

What does the bolded phrase mean here? I assume it does not mean the String is moved, so it must mean a &String is captured? If so, why is it misleading to say "closures which only take immutable references to captured variables"?

The compiler does not really know how many times a function will be called. FnOnce is defined in terms of move semantics: FnOnce::call_once takes self by value. The closure owns all of its captures and will drop everything after it runs. Therefore it would be unsound to call the function more than once. This property means that FnOnce can be called at most once. Taking self by value is the mechanism for enforcing the property.

Not speaking for quinedot, but a closure that owns its captures is still eligible for Fn if calling it does not drop the captures.

In this example, s is moved into the closure, so it cannot be used afterward. But the closure implements Fn because calling it does not drop the captured owned String.

It would drop if it moved s, but it just prints (Display::fmt takes &self):

2 Likes

There's two main ways the compiler decides which Fn traits to implement.

The first way is in a vacuum.[1] Instead of trying to look at how many times you call the closure, the compiler looks at the body of the closure and heuristically decides which traits to implement based on how the captures are used. Here's a description:

Closure types all implement FnOnce, indicating that they can be called once by consuming ownership of the closure. Additionally, some closures implement more specific call traits:

  • A closure which does not move out of any captured variables implements FnMut, indicating that it can be called by mutable reference.
  • A closure which does not mutate or move out of any captured variables implements Fn, indicating that it can be called by shared reference.

This heuristic is pretty good, but in niche circumstances it can result in an error.

The second a way the compiler decides which Fn traits to implement -- or try to implement -- is if the closure is defined in a position expecting a specific Fn trait. That's almost always an argument to some function or method that takes a generic with a Fn trait bound.[2] For example, Option::inspect takes a F: FnOnce(&T). If you call it like this (which is the usual way to call it):

option.inspect(|t| println!("{t}"))

Then the closure will only implement FnOnce, even though it could implement FnMut and Fn. This usually doesn't matter since you're giving away the closure anyway, but again there are niche circumstances where for example a FnMut closure may get created due to the context it was defined in, and then passed to something expecting a Fn closure, resulting in an error -- even if the closure could have implemented Fn.

Contrary-wise, if closure inference gets things wrong in some way, you can "funnel" a closure through a method expecting the correct implementation and correct the compiler's inference. On rare occasion this can be used to correct the "vacuum" inference of which Fn traits to implement. Much more frequently, this is used to correct the compiler's poor inference of closures which take borrows as arguments (especially if they also return those borrows).


OK, that was a lot of words. Here's some demonstration of these ideas with code.

Most of the time things just work and you don't need to know all the tricks. The "borrow-passing closure" pattern of the last example is the most common scenario that needs tricks (the funnel workaround).

But hopefully this shows you not just tricks, but also gives you a better understanding of what the compiler is doing.


Not at all.


  1. this will be more clear in a moment ↩︎

  2. Positions that don't expect a specific Fn trait use the "vacuum" approach above. ↩︎

3 Likes

No, I meant the String is moved, as in @parasyte's example. Turns out the reference page I linked in my last comment has a clarification lacking from the std docs:

Note: move closures may still implement Fn or FnMut, even though they capture variables by move. This is because the traits implemented by a closure type are determined by what the closure does with captured values, not how it captures them.

3 Likes

@parasyte @quinedot Thank you.

@quinedot Thank you for your great explanation.

This heuristic is pretty good

Indeed, a lot clearer than the std docs.rs (IMHO)

OK, that was a lot of words. Here's some demonstration of these ideas with code.

Thank you for this :smiley:

Most of the time things just work and you don't need to know all the tricks.

Personally, I like to know what I'm playing with, in order to better understand the problems I'm facing / may face in the future.

Thank you very much again

1 Like