Closures that return values that borrow from captures/args

As far as I can tell, creating closures that return values that borrow from captures and arguments is not possible in Rust today. I'm wondering if maybe it's possible to build a macro that allows something close to the conciseness of closures. But I'm thinking probably not since you can't seem to get at type inference information from a macro?

My main use-case is I'd like users of a library to be able to register async handler functions that own their captures & borrow them during their async block, and not have all the boilerplate of creating a struct + implementing a trait for each handler (not unreasonable to have hundreds of handlers in a given application).

Say we had a trait like this

pub trait BorrowFn {
    type Args<'a>;
    type Return<'a>;

    fn call<'a>(
        &'a mut self,
        args: Self::Args<'a>,
    ) -> Self::Return<'a>;
}

And say we wanted to write a closure like this:

let x = Box::new(42); // box so it's not Copy
let foo = move |y: &isize| {
    let x = &x; // Borrow x to prevent it from being moved into async block
    async move {
        if *x > *y {
            *x + 42
        } else {
            *y + 42
        }
    }
};

We can generate code that looks something like this:

#![feature(generic_associated_types)]
#![feature(type_alias_impl_trait)]

use std::future::Future;

pub struct Foo {
    x: Box<isize>,
}

impl BorrowFn for Foo {
    type Args<'b> = &'b isize;
    type Return<'b> = impl Future<Output = isize> + 'b;

    fn call<'b>(
        &'b mut self,
        args: Self::Args<'b>,
    ) -> Self::Return<'b> {
        async move {
            if *self.x > *args {
                *self.x + 42
            } else {
                *args + 42
            }
        }
    }
}

Ideally the macro invocation would like something like this:

let x = Box::new(42isize); // box so it's not Copy
let foo = borrow_fn! {
    |y: &isize| {
        async move {
            if *x > *y {
                *x + 42
            } else {
                *y + 42
            }
        }
    }
};

But then how does the the macro know what to assign as the Return<'a> type in the trait implementation? And the type of x in the Foo struct? It would need type information from the compiler.

Anyone have any clever ideas for how to do this? Or if someone has already done this? Or how herculean of a task it'd be to get closures like this properly into the language? I'm probably not qualified but I'd be willing to take a stab at an RFC and implementation.

1 Like

I don't think this can be done today. I will note that the unstable async closure feature is intended to do the same thing, where the returned future borrows from the closure, but I'm not sure of the details.

So I incorrectly assumed the Fn* traits would have to change to handle the lifetimes, but I got the traits to work (at least with FnOnce).

When I try to implement FnMut, rustc complains about different types for the type Output = impl Future<isize> + 'a type. But I figure since they're both the return type of fn bar(...), shouldn't they be the same type? Or is the 'a lifetime in the impl<'a> FnOnce ... block different from the one in impl<'a> FnMut?

#[derive(Clone)]
pub struct Bar {
    x: Box<isize>,
}

fn bar<'a>(s: Cow<'a, Bar>, y: &'a isize) -> impl Future<Output = isize> + 'a {
    async move {
        if *s.x > *y {
            *s.x
        } else {
            *y
        }
    }
}

impl<'a> FnOnce<(&'a isize,)> for Bar {
    type Output = impl Future<Output = isize> + 'a;

    extern "rust-call" fn call_once(
        self,
        args: (&'a isize,),
    ) -> Self::Output {
        bar(Cow::Owned(self), args.0)
    }
}

// Fails to compile
impl<'a> FnMut<(&'a isize,)> for Bar {
    extern "rust-call" fn call_mut(
        &'a mut self,
        args: (&'a isize,),
    ) -> Self::Output {
        bar(Cow::Borrowed(self), args.0)
    }
}

fn dummy() {
    use futures_lite::future;

    let mut ex = async_executor::Executor::new();

    ex.spawn(async move {
        let bar = Bar { x: Box::new(42) };
        let y = 43;
        let result = (bar)(&y).await;
        println!("result: {}", result);
    }).detach();

    // Fails to compile
    /*let x = 42;
    let f = move |y: &isize| {
        let x = &x;
        async move {
            if *x > *y {
                *x
            } else {
                *y
            }
        }
    };*/

    future::block_on(ex.run(future::pending::<()>()));
}

I mean, there's going to be a different Output type for every choice of the lifetime 'a, so we aren't speaking about one single type in the first place.

Ah right. But it is the same set of types then - correct?

for<'a> impl Future<Output = isize> + 'a with the impl Future being some specific anonymous struct that implements the Future trait?

Oh, I see. The problem is that you put 'a on &mut self in call_mut, which is not allowed by the trait.

Ahh yup. So the Fn/FnMut traits would have to change to support returning values that borrow from captures. So IIUC I don't think async closures will be able to support that without new traits.

Indeed, they wont.

Technically one can write any signature using FnOnce (for instance, Fn and FnMut are shorthands for advanced FnOnce signatures; F : FnMut… <=> for<'a> &'a mut F : FnOnce…, and same for Fn and for<'a> &'a F).

While Fn{,Mut} are indeed too restrictive (their parameters and return type cannot depend on 'a), the FnOnce trait, on the other hand, allows signatures such as

for<'a> &'a mut F : FnOnce(…) -> BoxFuture<'a, isize>

The issue, in practice, will lie in trying to instantiate such closures using the compiler-provided |…| … sugar: in those cases, Rust tries to infer a bunch of things, and, in practice, often fails to provide more advanced / subtle signatures, such as the ones above. That's where macros can help provide a distinct form of sugar.

2 Likes

Very cool I hadn't thought of that! impl<'a> FnOnce for &'a mut MyClosure works. But I can't seem to call it using function call syntax, I have to manually invoke FnOnce::call_once. I can live with that though.

impl<'a> FnOnce<(&'a isize,)> for &'a mut Bar {
    type Output = impl Future<Output = isize> + 'a;

    extern "rust-call" fn call_once(
        self,
        args: (&'a isize,),
    ) -> Self::Output {
        bar(Cow::Borrowed(self), args.0)
    }
}

Any ideas on how a nice macro sugar would work? Unless there's a way to get the type of a variable passed to a macro and place that type in generated code, best I can think of is to have caller manually supply captured values and their types, along with full fn signature. Then maybe have it detect any borrows and create a separate lifetime parameter for each, and add all lifetimes to the return type if the return type is of the form Container<dyn Trait> or impl Trait.

borrow_fn! {
    capture: {
        x: &isize
    }
    fn (y: &isize) -> impl Future<isize> {
        async move {
            if *x > *y {
                *x
            } else {
                *y
            }
        }
    }
}

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.