Declarative API with nested closures: difference between struct created in local scope vs struct returned by function

Full example in the playground

I'm trying to put together a declarative-ish API using closures, like this:

fn main () {
    // ok
    row(|add| {
        add(&One);
        add(&One);
    });
    col(|add| {
        add(&One);
        add(&One);
    });
    // not ok: 
    col(|add| {
        add(&row(|add| {
            add(&One);
            add(&One);
        }));
        add(&row(|add| {
            add(&One);
            add(&One);
        }));
    });
}

The problem is that add(&One) is valid but add(&row(|add| { add(&One) })) is not -- even though One matches &dyn Thing and row returns Thunk which also matches &dyn Thing. I get E0716:

error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:16:14
   |
15 |        col(|add| {
   |             --- has type `&mut Define<'1>`
16 |            add(&row(|add| {
   |  __________-____^
   | | _________|
   | ||
17 | ||             add(&One);
18 | ||             add(&One);
19 | ||         }));
   | ||          ^-- temporary value is freed at the end of this statement
   | ||__________||
   | |___________|argument requires that borrow lasts for `'1`
   |             creates a temporary value which is freed while still in use

What do I misunderstand about lifetimes of structs instantiated in the current scope, vs lifetimes of structs returned by functions called in the current scope, and is it possible to make the syntax work as shown?

All the temporary values you create in closures are destroyed before the closure returns, and therefore any results depending on them are also temporary and can't be kept.

Most likely you should use owned (self-contained) values, not temporary references. If you want to pass owned values "by reference", then Box<One> is the right type for it. If you want to reference something from multiple locations, then Rc<One> is the correct type. Rust's references are special and useful only in very limited circumstances.

Alternatively, if you really want to work with temporary scope-bound loans of pre-existing values, ensure all values are stored in an outermost scope:

fn main() {
   let one1 = One;
   let one2 = One;

   col(|add| {
        add(&row(|add| {
            add(&one1);
            add(&one2);
        }));
}

Anything creating lifetimes out of thin air is suspicious (ask yourself: where is it borrowing from?):

pub fn row <'a> (items: impl FnMut(&mut Define)) -> Thunk<'a> {

And indeed, you have problems beyond any temporary scope issues in main.

1 Like

I want to use stack allocation only by default; but I also need to support dynamic dispatch for the various different components that slot into the rows and cols. So I can't pass owned values because add takes &dyn Thing.

Moving the Ones from the nested case didn't work;. I still don't understand the difference between add(&One) and add(&row(...)):

// valid lifetimes:
col(|add| { add(&One)); })
// invalid lifetimes:
col(|add| { add(&row(|add|{/*nothing*/})); })

Some takeaways/principles from this might be:

  • it's not always the first error message you should focus on first, if there is only a handful of errors (and some seem hard to fix in isolation) always make sure to read the other errors, too. If it's too many errors, maybe you violated the next principle:

  • compile often. If you define a function and use it, then:

    1. define the function
    2. see if it compiles
    3. then use it

    the first two steps would be repeated (i. e. keep defining more and more if the function) for a complicated one

    In the example here, seeing if row or col compiles on its own would have set the better focus on those being problematic in the first place

  • using todo!() can be helpful for skipping the definition of functions. In that case, e. g. if todo would've been used for row and col, then: a function implemented with todo might still have a signature that's hard or impossible to implement, so:

  • if a function is hard to use, especially if lifetimes are involved, it can make sense to question its signature. Is it what you want? Can it be implemented (here's where presence of todo would indicate this point could need more attention)? Even if it can be implemented, function signatures, especially with lifetimes, can sometimes still be wrong, and the signature too restrictive for the caller. Typical cases: two lifetimes are made the same without need and in a way that hinders callers. Or there are more where clauses than actually necessary. Or a trait object or impl Trait return type might miss bounds like Send even though typical use cases might need it.

1 Like

Here is a sketch of what initializing a purely stack-allocated tree structure will generally look like in Rust:

let x = foo("Hello", "world");
let y = foo("this", "is");
let z = foo("a", "demonstration");
let root = bar(&x, &y, &z);

Each thing you take a reference to must be stored in its own named variable so that it will live long enough. (And you then cannot use w any longer than up to the end of the current scope.)

There are also GUI systems in Rust that work with nested closures like you have — but they are immediate mode, meaning that the items are drawn while the functions are still running — not saved in a tree for later.

1 Like

You mean these:

error[E0515]: cannot return value referencing temporary value
  --> src/main.rs:30:5
   |
30 |     Thunk { execute: &|_1,_2|{println!("{items:?}"); Ok(())} }
   |     ^^^^^^^^^^^^^^^^^^--------------------------------------^^
   |     |                 |
   |     |                 temporary value created here
   |     returns a value referencing data owned by the current function

Indeed. That was the next thing.

But I was hoping to clarify the difference between:

  • add(&One) and
  • add(&row(|add|{})) first.

One implements Thing, row returns Thunk which implements Thing, why is an in-place instantiated struct valid, and a struct returned from a function is not?

It's borrowing from its parent struct, kinda like this:

struct Foo {
    a: One,
    b: One
}
impl Foo {
    fn layout (&self) -> Thunk {
        row(|add| {
            add(&self.a);
            add(&col(|add|{
               add(&self.b)
               add(&One)
            })
        })
    }
}

Which is, of course, out of the scope of the example, because I'm doing exactly what @steffahn says and want to clarify one thing at a time, starting with the difference between add(&One) and add(&row(...)).

Sorry if I've failed to trim down the example sufficiently. Removing the 'a lifetime makes the dynamic dispatch in Thunk impossible (but maybe I should change that to a <F: Fn(...)> generic bound).

EDIT: So here's an example which removes the 'a lifetime and just makes row and col return One. Now there's no difference. I'll try to build the thunks back into it.

Here's a playground with the Thunk issues fixed.

As for the other errors, we have:

    col(|add| {           // ClosureOne
        add(&row(|add| {  // ClosureTwo
            add(&One);
            add(&One);
        }));
        add(&row(|add| {  // ClosureThree
            add(&One);
            add(&One);
        }));
    });

What is the type of add as a parameter of ClosureOne? Well, ClosureOne is an impl FnMut(&mut Define<'_>). That means the arguments are of type &'x mut Define<'y> for some unknown lifetimes 'x and 'y. What you do know is that

  • 'y outlives 'x so the reference is valid ('y: 'x)
  • 'y is invariant (cannot be "shrunk" -- coerced to be shorter) as it is behind a &mut
  • 'x and 'y must be nameable by the caller -- and thus they must be longer than the closure body

Now, Define<'a> can only be called with a &'a T as per your trait implementations. Because it's storing these references, it would be possible to have it take some &'b T longer than 'a, but never anything shorter.

Altogether this means that when you call the outer add(...), you have to pass it a reference with the lifetime 'y -- the lifetime of add's Define<'_> -- and that lifetime is longer than the closure body.

So it's impossible to pass a reference to a local variable like the result of row(...) -- whether or not it's a temporary.


What's the difference from passing &One? &One is a constant value that can be promoted to a &'static One. That reference lifetime isn't limited to the closure body, like borrows of local variables are.

2 Likes

Hmm, good point. I didn't realize that this structure was suitable only for immediate mode. I like its brevity. Implementation-wise, I'm trying to compute the sizes in advance but save the rendering for later - while at the same time trying to minimize the complexity of declaring the layout.

For example.

Thanks, this looks like the explanation I was looking for. I'll take some time to wrap my head around this and gain the intuition I was missing about closure lifetimes.

I think this made it click.

Bummer... makes sense though. And since I need to pass references in order to use dyn dispatch, there isn't much I can do to keep the "nested closures" syntax?

I doubt it (but it's not entirely clear to me what the goal is).

Incidentally here's another lifetime-out-of-nowhere I just noticed:

impl<'a> Wrapper<'a> {
    fn collect (mut items: impl FnMut(&mut Define)) -> Vec<Self> {

The call to items(&mut define) can't actually add anything to the Vec unless it's a Wrapper<'static>.

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.