Give capturing closures transparent types

Why can’t the type of capturing closures be named? What is the reason for opaque types like 10:78 x:_, y:_]?

Couldn’t capturing closures be implemented with types like:

struct Capturing<E, F> {
    environment: E,
    fn_ptr: F,
let x = 1;
let y = 2;
let lambda: Capturing<(&isize, &isize), fn(&isize, &isize, &isize) -> &isize> = |n| x + y + n;
let mut foo = "foo";
let mut make_bar: Capturing<(&mut usize), fn(&mut str)> = || foo = "bar";
let foo = "foo".to_owned();
let foo_with: Capturing<(String), fn(String, String)> = move |suffix: String| println!("{}{}", foo, suffix);
// Relevant impls would be generated; E.g:
impl FnOnce(String) for Capturing<(String), fn(String, String)> {}

Currying could be implemented by generating implementations of Fn* traits for each case/arity of partial application.

Am I missing something that requires capturing closures be opaque?

Closures don’t contain a function pointer, their Fn trait implementation directly contains the code.

fn_ptr could be named func and be marked ?Sized to support inline fully applied functions.

I would recommend reading my blog to see how closures are desugared before making changes to it. My blog gives a detailed account of how closures are desugared, including little bits sich as mhow the move keyword interacts with closures.

Basically closures have a hidden enviornment parameter which is made explicit by the Fn* traits, so the struct containing the captured enviornment only needs to contain the enviornment, no need to store a function pointer.

1 Like

Because for each closure, the compiler creates a new environment struct;

If you really need explicit types for your closures, then you have to do the unsugaring yourself instead of letting the compiler do it.

What currently prevents us from doing it in stable is the fact that Fn traits are unstable: it is still unclear whether their current design is good enough to become future-proof; it may thus change in the future, which would be a breaking change if it were stable.

Obviously stable Fn traits would be a very sweet thing to have, indeed.

This may be surprising, but monomorphisation is that powerful of a tool that you don’t require function pointers around, it’s all done statically (this is one of the places where Rust just blatantly outperforms C).

Since @Yato’s blog post already covers closures, I will give an example of monomorphisation vs parameter that does not involve closures, since they are not needed to show the example:

Example (playground)

  1. Dynamic argument

    fn repeat_hi_dynamic (n: usize)
        (0 .. n).for_each(|_| say_hi());

    gives the following assembly:

    playground::repeat_hi_dynamic: // Argument is %rdi
        pushq   %rbx
        testq   %rdi, %rdi // if rdi
        je  .LBB5_3  // == 0 { return; }
        movq    %rdi, %rbx // let mut rbx = rdi;
    .LBB5_2: // loop {
        callq   playground::say_hi // say_hi()
        addq    $-1, %rbx // rbx -= 1;
        jne .LBB5_2  // if rbx != 0 {} else { break; }
     // }
    /// return;
        popq    %rbx
  2. Static (type-level) argument

    trait TypeLevelUsize { const value: usize; }
    fn repeat_hi_static<N: TypeLevelUsize> (_: N)
        (0 .. N::value).for_each(|_| say_hi());
    • Example of static argument; the number 1:
      struct One; // Zero-sized struct: no dynamic data!
      impl TypeLevelUsize for One {
          const value: usize = 1;

    gives, with a monomoprhization to One, the following assembly:

    /// repeat_hi_static<One> : fn()
        jmp playground::say_hi

That is, even though we call it with a One argument (c.f. repeat_hi_static(One); ), the "number 1" information of One is not within its struct but within its type (in a similar vein as to why a &[u8; 4] contains just an address whilst a &[u8] contains both an address and a size).

In a similar fashion, a closure’s "call function" is relative to the type of the closure.


This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.