Search code examples
pointersrustclosures

Why is calling Box-ed closure requires unstable fn_traits?


Consider the following code:

#![feature(fn_traits)]
fn test_fn_2_args<F>(f: Box<F>)
where
    F: Fn(i32, i64) -> i32
{
    <Box<F> as Fn<(i32, i64)>>::call(&f, (1, 2));
}

fn main(){
    test_fn_2_args(Box::new(|first, second| {
       println!("{first}, {second}");
       first
    }));
}

Playground

This code requires unstable fn_traits, but this is not clear why. As per fn_traits docs it's designed for implementing closure-like type which I don't do in this example.

All I do is interpret Box<Fn> as Fn which is not clear why is allowed as well since Box<Fn> does not implement Fn obviously.


Solution

  • Calling a boxed closure, does not require unstable. To call it, you simply use the standard calling syntax (i.e. a call expression) to call it. Boxed or not.

    It doesn't matter whether you have Fn() or Box<dyn Fn()> you call it in the same way, i.e. f().

    The reason you can call them in the same way, is because there is a blanket implementation of impl<F> Fn for Box<F> where F: Fn.

    The reason you can use Fn, but not Fn::call() is because Fn is stable, while Fn::call() is unstable. Which as you've discovered is part of the unstable fn_traits feature (see issue #29625).

    So the issue is not "interpreting Box<Fn> as Fn", the issue is you're attempting to call the unstable Fn::call().

    In short, doing this is stable:

    fn f<F: Fn()>(f: F) {
        f();
    }
    
    fn boxed_f<F: Fn()>(f: Box<F>) {
        f();
    }
    

    While doing the following is unstable:

    fn f<F: Fn()>(f: F) {
        Fn::<()>::call(&f, ());
    }
    
    fn boxed_f<F: Fn()>(f: Box<F>) {
        Fn::<()>::call(&f, ());
    }
    

    All in all, just call the closure like any other function:

    fn test_fn_2_args<F>(f: Box<F>)
    where
        F: Fn(i32, i64) -> i32,
    {
        f(1, 2);
    }
    

    You don't even need Box:

    fn test_fn_2_args<F>(f: F)
    where
        F: Fn(i32, i64) -> i32,
    {
        f(1, 2);
    }
    

    Again, since there is a blanket implementation of impl<F> Fn for Box<F> where F: Fn. That also means that changing test_fn_2_args() to not use Box, still allows you to call it using a boxed closure. So the following compiles perfectly fine.

    fn test_fn_2_args<F>(f: F)
    where
        F: Fn(i32, i64) -> i32,
    {
        f(1, 2);
    }
    
    fn main() {
        test_fn_2_args(|first, second| {
            println!("{first}, {second}");
            first
        });
    
        test_fn_2_args(Box::new(|first, second| {
            println!("{first}, {second}");
            first
        }));
    }