Fn vs fn in type signatures

I used to believe, in the context of types, Fn/fn were the same thing, i.e.

Fn(i32, i32) -> f32
vs
fn(i32, i32) -> f32

However, they seem to be different things even in the context of types. Where can I read up more on this?

The Fn trait's page and the fn pointer's page. One is a trait, the other is a primitive like &T.

2 Likes

Got it, so:

  1. Fn is a trait, fn is a type.

  2. static functions can be either fn or Fn

  3. closures can be Fn, but can not be fn

Are all three above correct?

fn is a way of introducing a type, in the same way struct or enum can. Each function signature is its own type.

Types implement traits, like Fn, FnMut, FnOnce, Copy, and more. In some cases those impl's are explicit, in some cases they're inferred or applied automatically, as is the case when closures are used.

When putting bounds on the type of an argument, we can be generic using traits so that any type that matches the trait can be passed.

2 Likes

Not true, if a closure doesn't capture anything ot can be fn.

4 Likes

fn is a function pointer, and Fn is a trait for things that callable from its shared reference. Typically closures capture their environment so they're more than just function pointer. But if they don't, it's just plain function pointer so it can be fn.

1 Like

Since one is a type and the other one is a trait,

  1. The former can be used in type position:

    //! C-style callbaks
    
    fn call_once (cb: fn())
    {
        cb();
    }
    
    fn call_twice_sequentially (cb: fn())
    {
        cb();
        cb();
    }
    
    fn call_twice_in_parallel (cb: fn())
    {
        ::crossbeam::scope(|scope| {
            scope.spawn(|_| cb());
            scope.spawn(|_| cb());
        }).unwrap();
    }
    

    The three functions above just take a (function) pointer as argument.

  2. The latter, since it is not a type, can "reach the type realm" through generics (or impl trait sugar), or trait objects;

    //! Rust style "callbacks" for maximum flexibility
    
    fn call_once<Env> (env: Env)
    where
        Env : FnOnce(),  // input environment is callable by value (thus consuming it)
    {
        env();
    }
    
    fn call_twice_sequentially<Env> (mut env: Env)
    where
        Env : FnMut(),  // input environment is callable by (unique) reference
    {
        env(); // first borrow begins and ends here: &mut possible
        env(); // second borrow begins and ends here
    }
    
    fn call_twice_in_parallel<Env> (env: Env)
    where
        Env : Fn(), // input environment is callable by (shared) reference
        Env : Sync, // environment can be *shared* across thread boundaries.
    {
        ::crossbeam::scope(|scope| { // in parallel, do:
            scope.spawn(|_| env()); // one shared borrow here
            scope.spawn(|_| env()); // another one here
        }).unwrap(); // both borrows end here
    }
    

    The three functions above can be monomorphized into taking any kind of environment as argument, so as long as that environment is callable: a closure.

Finally, a function pointer cannot have a captured environment, thus

fn() = "empty" (zero-sized) environment : Fn

9 Likes

In other languages Fn would be called a Callable interface. fn is just a function type.

5 Likes

Another way to think about it is like this:

  • Fn* is a trait and is an operation. Meaning that this is a syntactic sugar trait, like Add, Mul, Sub, or Div, or even Deref and DerefMut. The sugar that this trait applies is the ability to be called like so:
fn foo(data: impl Fn<(usize, ), Output=usize>) {
    println!("{}", data());
}
  • fn pointers are just pointers to function. Not to be thought of as objects which just so happen to implements a Fn* trait, they are solely function pointers. IE, they point to the entrance of a function and that's it. (And yes, they are technically just references which implement the Fn* traits...)
fn foo(data: fn(usize) -> String) {
    println!("The result is {}", data(30));
}
fn double_and_fmt(num: usize) -> String {
    format!("{}", num * 2)
}
fn main() {
    foo(double_and_fmt);
    foo(|x| "This is a closure".to_string());
    let x = 3;
    foo(move |z| {format!("{}", z + x)}; //This won't work.
}

And the last line in main will not work because it now is an object with a state, not just a function pointer. The call to foo with a closure that doesn't move is usually done like this internally:

fn __closure_at_line_col(x: usize) -> String {
    "This is a closure".to_string()
}
fn main() {
    // -- snip --
    foo(__closure_at_line_col);
    // -- snip --
}
5 Likes

I'm surprised nobody has mentioned this, but Fn actually is also a type. All traits are. However, spelling them this way is deprecated; the proper name for it is dyn Fn.

// These are equivalent.
type OldSyntax = Fn(usize) -> i32;  // this is deprecated
type NewSyntax = dyn Fn(usize) -> i32;

dyn Trait is what we call a trait object; it's basically a type with a vtable for the purposes of dynamic polymorphism. This type is "unsized" and you can only refer to it behind a pointer. (e.g. Box<dyn Trait> or &dyn Trait)

This type should seldom show up in public interfaces (it is mostly useful for functions stored in private fields), but it does have some niche use cases. (e.g. to make a function monomorphic, or to prevent an infinite type from appearing in a recursive function)

8 Likes

It would be nice to stop teaching the old spelling entirely so new people don't get confused on when to use the syntax. It can still be documented somewhere, but not taught. Also the old spelling will eventually become a compiler error, so we should start transitioning away from the old spelling.

6 Likes

A post was split to a new topic: Difference between fn and Box<dyn Fn>