Function taking another function as argument in a generic struct


#1

Hi everyone,

I am writing a library whose users will be able to specify one function to compute some values and a second function to call on the first one. My library will provide a struct to hold both and simplify the calculations.

I am building this step by step. First, I write two standalone functions and this works:

fn double(x: f32) -> f32 {
    x * 2.0
}

fn do_call<F: Fn(f32) -> f32>(f: F, x: f32) -> f32 {
    f(x)
}

fn main() {
    println!("{}", do_call(double, 3.0));
    println!("{}", do_call(&double, 3.0)) //Note that & also works
}

Let’s wrap this inside a struct:

fn dbl(x: f32) -> f32 {
    x * 2.0
}

fn do_call<F: Fn(f32) -> f32>(f: F, x: f32) -> f32 {
    f(x)
}

struct Lazy<F, G>
    where F: Fn(f32) -> f32,
          G: Fn(F, f32) -> f32
{
    callee: F,
    caller: G
}

impl<F, G> Lazy<F, G>
    where F: Fn(f32) -> f32,
          G: Fn(F, f32) -> f32
{
    fn call(&self, x: f32) -> f32 {
        (self.caller)(&self.callee, x)
    }
}

fn main() {
    let l = Lazy{callee: dbl, caller: do_call};
    
    println!("{}", l.call(3.0));
}

This fails with:

error[E0308]: mismatched types
  --> <anon>:22:23
   |
22 |         (self.caller)(&self.callee, x)
   |                       ^^^^^^^^^^^^ expected type parameter, found &F
   |
   = note: expected type `F`
   = note:    found type `&F`

error: aborting due to previous error

Why does & not work now? The bounds on F are the same, as far as I can see.

Thanks.


#2

The first snippet works because there are two different specializations of do_call generated - one that accepts a Fn(f32) -> f32 argument, and one that accepts a &Fn(f32) -> f32 argument. This doesn’t work in the second snippet, because the types of F and G in Lazy::call are determined by the types originally stored in the Lazy struct, so there is a mismatch when you try to pass a different type as an argument.

You can work around the problem you’re seeing by adding the + Copy bound to both F and G for Lazy::call and remove the & from &self.callee.


#3

I wonder if the better formulation is the following:

fn dbl(x: f32) -> f32 {
    x * 2.0
}

fn do_call<F>(f: &F, x: f32) -> f32 where F: Fn(f32) -> f32 {
    f(x)
}

struct Lazy<F, G>
    where F: Fn(f32) -> f32,
          G: Fn(&F, f32) -> f32
{
    callee: F,
    caller: G
}

impl<F, G> Lazy<F, G>
    where F: Fn(f32) -> f32,
          G: Fn(&F, f32) -> f32
{
    fn call(&self, x: f32) -> f32 {
        (self.caller)(&self.callee, x)
    }
}

Since Lazy is going to own both the caller and callee, I think this would be better to avoid unnecessary closure copies?


#4

Thanks for the explanation, the cause of the error is clear now! I wasn’t realising that the reference was in fact instantiating a different type, I thought that some coertion was taking place.