Difference between fn pointer and fn item

In the code below

type Record = [i32; 3];

fn reducer(partition: &[Record]) {
    /* Some implementation */
}

fn on_event(callback: &fn(&[Record]))  { 
    /* Some implementation that generates data*/ 

    callback(&data);
}

pub fn main() {
   /* Some code */
   on_event(&reducer);
}

This errors out with:

 on_event(&reducer);
          ^^^^^^^ expected fn pointer, found fn item

However, after updating as documented here - https://doc.rust-lang.org/reference/types/function-pointer.html - it tends to work. What exactly is happening behind the scenes that is causing the trouble in the original code?

Update: Found a reported issue - https://github.com/rust-lang/rust/issues/51233 - which is yet to be addressed.

Update-2: Removing the reference from the type declaration of callback also gets it to work:

fn on_event(callback: fn(&[Record]))  { } // No & before fn

// Inside main
on_event(reducer) // No & before reducer
1 Like

The fn item is something that uniquely identifies the specific function you are passing - in that way it can help with optimization when they are used, since the identity of the function is not lost and the call can always be compiled to a direct call.

When we use function pointers, we call the function through a pointer, at least formally, which can be an indirection. All functions with the same signature can be stored in the same function pointer type. (The compiler can sometimes arrange so that it optimizes just as well as the first case.)

fn items convert readily to fn pointers, but as you have found there are some corner cases where it doesn't happen as it should. You can make the conversion explicit like this: on_event(&(reducer as fn(_)))

If you don't have any other restrictions here, I think it would be a good solution to use neither of these two, and use the closure traits. That should both be more idiomatic and flexible.

Like this:

fn on_event(mut callback: impl FnMut(&Record)) {
    /* Some implementation that generates data*/ 

    callback(&data);
}

This should work very well for a callback. I've picked the FnMut closure trait, which all functions implement and most closures implement.

2 Likes

To call a function one needs an address. The question is, how do we provide the address to a function? We can provide it as a runtime value to a single function, or as no value to a myriad of different functions, with each one having the right value hardcoded in it. The former are fn() pointers, whereas the latter are fn() "items".

See this Playground for an example doing the same with u64s:

fn foo<T>(_: T)
where
    T : TypeLevelConstant,
{
    println!(
        "foo<{:?}> got {}",
        ::std::any::TypeId::of::<T>(),
        T::VALUE,
    );
}

fn bar (value: u64)
{
    println!("bar got {}", value);
}

fn main ()
{
    let ft = type_level_constant!(42);
    let ts = type_level_constant!(27);
    foo(ft); // foo<ZST_ft>
    foo(ts); // foo<ZST_ts>
    bar(ft.into());
    bar(ts.into());
}

So, in this example, ft and ts, on runtime, are zero-sized, but at compile time they contain a u64 as (static) information within their types; this is what allows the foo(ft) and foo(ts) calls to not be giving any runtime information, thanks to, instead, monomorphising foo into two distinct functions, each one with the value hardcoded in it. This is the equivalent of fn items.

Whereas bar is a single function, called each time with a different runtime parameter. This is the equivalent of a function taking a fn() pointer.

As you can see, it is possible to convert the type-level constant into a runtime one (.into() in my example, but in the case of fn items a simple coercion suffices).

3 Likes