Finding Closure in Rust

Closures in Rust are powerful and flexible, building on traits, generics and ownership.

6 Likes

Nice content. Good to know people write about Rust!

If closures map onto ref, ref mut and move, then why is FnOnce not called FnMove?

Discussion here.

That's a great article! There is one bit I cannot quite understand, though:

Since each closure has its own type, there’s no compulsory need for heap allocation when using closures: as demonstrated above, the captures can just be placed directly into the struct value.

How does each closure having a distinct type eliminate the need for heap allocation? Let's say i32 and Vec have different types, but both are by default created on the stack (unless you use something like Box)

As an additional question: what's the tradeoff between stack/heap allocation? I understand that heap allocation requires a runtime (you need to call malloc from libc), but what does it offer over stack allocation?

Anything created on the stack is a local variable of some function. When that functions returns all it's local variables are removed from the stack. So things created on the stack are not available to the functions caller, they have a life time only as long as the function takes to run. Which is why returning references to local variables is a no-no.

Things created on the heap can outlive the function. A function can return a Box of something.

That thing about closure types and stack vs heap is an interesting question.

2 Likes

About the distinct closure types, imagine all closures Fn(usize) -> bool shared the same type. That would mean they all had the same size. However, the following two closures clearly have different storage requirements:

    let closure1 = |i: usize| i % 2 == 0;
    let data = [true, false, true, false, false];
    let closure2 = move |i: usize| data[i % data.len()];

The first closure needs no storage at all, the second closure needs five bytes to store the array. Now to force all these closures into the same size you would have no choice but to store all state on the heap.

2 Likes

@ergates Well... the quote from the article says "there’s no compulsory need for heap allocation", but you are suggesting "there is a compulsory need for heap allocation"! :stuck_out_tongue:

According to the article, Rust can put closures on the stack by placing the captures into an implicit anonymous struct.

There is no compulsory need for heap allocation if the type you are using is the anonymous type associated with the closure. The main place where heap allocation helps is that two different closures have two different anonymous types, so you can't have a variable that can contain one or the other, since there's no appropriate choice of type. However, with heap allocation, it is possible to do this, since you can use the Box<dyn Fn()> type, which any closure can be converted into.

1 Like

Hi @nalzok!

The article said

Since each closure has its own type, there’s no compulsory need for heap allocation when using closures: as demonstrated above, the captures can just be placed directly into the struct value.

I was trying to illustrate this by showing how having multiple closures share the same type would leave no choice but to put the captures on the heap

1 Like

@ergates Oh sorry I misread. That makes sense now! :wink:

1 Like

To summarize, there are two things that you may sometimes need for a closure (that is, an instance of some type that implements the Fn… interface):

  • on the one hand, you may need to return it from the current function, feed it to some API that may call it arbitrarily late (in Rust parlance, for this last requirement, we will say that the type of the closure needs to be 'static);

  • on the other hand, you may need to unify the closure types of different closures into one. In Rust this is achieved through dyn Traits / trait "objects", which are a type-erased (unified) version of an instance, where the only thing we can do after the erasure is use the trait in question. In the case of closures, this means having to deal with the dyn Fn… type.

    But dyn traits, for technical reasons, need to be used behind indirection / behind a pointer.

    So, this last need means, that if we take @ergates example, if we wanted closure1 and closure2 to be usable in the same place and thus require that they have the same type (e.g., to push both closures onto the same Vec), we could perform this through-indirection:

    let closure1 = move /* nothing */ |i: usize| i % 2 == 0;
    let data = [true, false, true, false, false];
    let closure2 = move /* data */ |i: usize| data[i % data.len()];
    
    // Errors:
    // let vec: Vec<???> = vec![closure1, closure2];
    // OK:
    let vec: Vec<&dyn Fn(usize) -> bool> = vec![&closure1, &closure2]; // OK
    

    So, indirection (to perform dyn-erasure) is the key to single type unification.

This last point, alone, does not necessarily mean that heap allocation (in Rust, the Box, Rc or Arc pointer types, mainly) needs to be involved.

The issue comes when both this requirements happen at the same type:

  • In order to feature type unification, we need indirection. But we if we don't resort to heap-allocated indirection (Box, Rc, Arc, etc.), then we have to use stack-allocated / borrowing indirection (as in my example above): &, &mut, etc.

  • In order to return a value from a function, in this case, a closure having captured some env, then the closure needs to have no local borrows whatsoever, e.g., it needs to own its captures.

That's, thus, when Box (or other heap-allocated) indirection is most often needed.

3 Likes

This topic was automatically closed 30 days after the last reply. We invite you to open a new topic if you have further questions or comments.