-1

Why accept fn(..) instead of Fn(...) in generics in Rust?

My intuition is that

  • on one hand if code accepts closure with context -> Fn(), but we provide "no context" closure fn(...) then compiler should optimize for fn(...).
  • on the other hand, maybe there are advantages on actual memory and assembly representation of fn(...) (e.g. takes less space, due to no need to store context), that would make us prefer to restrict our generic type to accept fn(...) types not `Fn(...) types ?

Therefore:

  • When one may want to restrict type to fn(...) in generic types?
  • When it brings difference,
  • and what kind of difference?

Below some pseudocodes for inspiration, feel free to polish them , this is not limited to those cases, as maybe memory size difference is better to demonstrate with Vec<dyn fn(X)->Y> vs Vec<Fn(X)->Y> (or using unboxed_closures syntax ;) ).

Sketches, examples, but not limited to those:

// peusdo code
pub struct Foo <F,X,Y>
where
F: fn(X)->Y ,
...
{
    fx: F,
}

impl Foo<F,X,Y> where
F: fn(X)->Y,
...
{
    pub fn new(foo: F) -> Self;
    ...
}

// peusdo code
pub struct FooByRef <'a,X,Y>
where
F: &'a dyn fn(X)->Y ,
...
{
    fx: F,
}

impl Foo<'a,X,Y> where
F: &'a dyn fn(X)->Y,
...
{
    pub fn new(foo: &'a dyn fn(X)->Y) -> Self;
    ...
}
Grzegorz Wierzowiecki
  • 10,545
  • 9
  • 50
  • 88
  • 8
    `fn(T)->U` is not a trait, but a concrete type. Given that, your examples not only fail to compile, but don't really make sense. – user4815162342 May 31 '21 at 18:31
  • 1
    Is this just about which is technically faster? I think the best answer to that question is: function pointers (`fn`) should theoretically be at least as fast as the `Fn` trait. However, definitely go with the `Fn` trait 99% of the time since it lets you use closures, and I would imagine that the performance is nearly identical after optimizations. – Coder-256 Jun 01 '21 at 00:32
  • Thank you, both above answers are insightful despite just comments :) – Grzegorz Wierzowiecki Jun 01 '21 at 08:27
  • 1
    There is no reason to assume function pointers are faster than generics. The compiler monomorphizes generics for every concrete type that is actually used, so there is no overhead. You only get overhead when using dynamic dispatch, i.e. `Box U>` or similar. – Sven Marnach Jun 01 '21 at 09:28
  • 1
    I started summarizing comments in answer below, which is set to be "community wiki" https://stackoverflow.com/a/67786735/544721 – Grzegorz Wierzowiecki Jun 01 '21 at 10:01
  • This question seems unfocused, and as pointed out already the code examples don't make sense. I wrote an answer at https://stackoverflow.com/questions/64298245/in-rust-what-is-fn which may help you understand better what's up with all these function types. – trent Jun 01 '21 at 17:50

2 Answers2

2

Here are some examples of how fn() and Fn() can be used in structs.

struct Foo<F>
// there is no need for trait bounds in a struct definition
{
  function: F,
}

// the impementation is generic over a generic type `F`, hence `impl<F> Foo<F>`
// `impl Foo<F>` would be an implementation for a concrete type called `F`
impl<F> Foo<F>
where F: Fn()
{
  fn new(function: F) -> Self {
    Self { function } // equivalent to `Foo { function: function }`
  }
  fn call_function(&self) {
    (self.function)();
  }
}

Foo requires a specific type which implements Fn() in order to be used. This makes Foo restricted to the single specific type, be it fn() or the type of one concrete closure or function. The restriction, however, makes it possible to store only the context of F since the implementation can be inferred from the type.

struct Bar {
  function: fn(),
}

impl Bar {
  fn new(function: fn()) -> Self {
    Self { function }
  }
  fn call_function(&self) {
    (self.function)()
  }
}

Bar is equivalent to Foo<fn()>, the only thing stored in it is the function pointer. It is restricted to functions and closures with no context, which can be cast to fn().

struct DynRefFoo<'a> {
  function: &'a dyn Fn(),
}

impl<'a> DynRefFoo<'a> {
  fn new(function: &'a dyn Fn()) -> Self {
    Self { function }
  }
  fn call_function(&self) {
    (self.function)();
  }
}

DynRefFoo may contain references to multiple types across its instances. For this reason, pointers to both the context and the implementation of the Fn() trait have to be stored in the struct.

In terms of the Fn trait, the context of fn is the pointer and the implementation simply calls the function behind the pointer.

Additionally, each function has its own zero-sized type which implements Fn so there is a slight difference between using a concrete function and a pointer to that function as dyn Fn.

To sum up:

  • dyn Fn works for everything.
  • fn can store functions and no-context closures only.
  • Fn bounded generic types are limited to a single closure since each closure has its own type.
Tesik
  • 68
  • 5
0
  • fn is concrete type as it has defined signature
  • Fn is trait - it has not fully defined/concrete signature, as it may change depending on captured context, what is resolved on use by compiler
  • fn is function pointer, so it consumes usize::BITS of memory
  • "There is no reason to assume function pointers are faster than generics. The compiler monomorphizes generics for every concrete type that is actually used, so there is no overhead. You only get overhead when using dynamic dispatch, i.e. Box<dyn Fn(T) -> U> or similar"
Grzegorz Wierzowiecki
  • 10,545
  • 9
  • 50
  • 88