Function with a generic type as a callback

Hello,

I would like to have a function that receives a generic type object and returns it but I don't know how to express it:

use serde::de::DeserializeOwned;

pub struct MetricConsumer<M, F>
where
    M: Send + Sync + DeserializeOwned,
    F: FnMut(M) -> M,
{
    url: String,
    transform: Option<F>,
}

impl<M: Send + Sync + DeserializeOwned, F: FnMut(M) -> M> MetricConsumer<M, F> {
    pub fn new(url: String, transform: Option<F>) -> Self {
        Self { url, transform }
    }

    pub async fn consume(&self, data: String) {
        let metric_obj: M = serde_json::from_str(&data).unwrap();
        let transformed = match &self.transform {
            None => metric_obj,
            Some(t) => t(metric_obj),
        };
        println!("Do more stuff with transformed");
    }
}

I don't know how to define the type of the transform attribute. It tells me that the M in the type definition is not used (link to the playground)

Thanks in advance!

For your M is not used this is a good use of std::marker::PhantomData is for it will allow you to tie M to a specific type when you use MetricConsumer even though it is not directly used. Then the compiler will complain about some other & versus &mut but cross that bridge when you get there as it will somewhat depend on what the rest of the code will be doing. playground

Thanks! That's exactly what I needed.

1 Like

Given that your type parameter F is FnMut(M) -> M, it is better to use PhantomData<fn(M) -> M> rather than PhantomData<M> (and so on)

2 Likes

What is the difference between Fn(M) ->M and fn(M) -> M?

If you mean to ask about the difference between PhantomData<M> and PhantomData<fn(M) -> M>, it has to do with a fairly advanced Rust notion:

Subtyping and Variance

To take a simpler example, let's imagine that M = &'s str, so that now we "just" have a generic lifetime parameter rather than a whole generic type parameter.

Let's simplify your MetricConsumer into a newtype wrapper over a F : FnOnce(&'s str):

struct Function<'s, F : FnOnce(&'s str)> (
    F,
    Phantom<'s>,
);

What should Phantom<'s> be?
PhantomData<&'s str> or PhantomData<fn(&'s str)>?

Let's see why it shouldn't be PhantomData<&'s str>.

We can call this function with a caller-chosen 's thanks to the following function:

fn call<'s, F> (function: Function<'s, F>, s: &'s str)
where
    F : FnOnce(&'s str),
{
    (function.0)(s)
}

And now the interesting part:

  1. imagine having some fn(&'s str) function, and a &'static str string

    fn foo<'s> (f: fn(&'s str), s: &'static str) {
    
  2. you happen to wrap your function into the Function wrapper struct:

    let function: Function<'s, _> = Function(f, PhantomData);
    
  3. and now you want to call it:

    call::<'static, _>(
        // `f: Function<'s, _>`, but the required type here is:
        // `Function<'static, _>`
        function,
        string,
    );
    

This ought to be fine: if the function f is able to handle an input string with a short lifetime 's, then it is sound for f to receive an input string which is valid for an even longer lifetime ('static).

That is, the type fn(&'s str) can be seen as a fn(&'static str), given that 'static : 's.

This is what we call contravariance!

Contravariance example

type F<'lifetime> = fn(&'lifetime str) verifies that
'static : 's ⟹ F<'s> : F<'static>
(the subtyping order is reversed).

This form of variance is opposite to the more classic one, covariance:

Covariance example

type F<'lifetime> = &'lifetime str verifies that
'static : 's ⟹ F<'static> : F<'s>
(the subtyping order is kept as-is)

So, when defining Phantom<'s> as PhantomData<&'s str>, Phantom<'s> is not contravariant in 's, making the Function wrapper not contravariant in 's, which makes the above foo() function fail to compile and yield:

error[E0308]: mismatched types
  --> src/lib.rs:31:9
   |
31 |         function,
   |         ^^^^^^^^ lifetime mismatch
   |
   = note: expected type `Function<'static, fn(&str)>`
              found type `Function<'s, fn(&str)>`
note: the lifetime 's as defined on the function body at 25:8...
  --> src/lib.rs:25:8
   |
25 | fn foo<'s> (f: fn(&'s str), string: &'static str)
   |        ^^
   = note: ...does not necessarily outlive the static lifetime

That's why we should define Phantom<'s> as PhantomData<fn(&'s str)>: Phantom<'s> is indeed contravariant in 's, letting the Function wrapper be so. The code then compiles just fine.

1 Like

Wow. I'll save this for later so I can read it when I can gather all my mind juice to try to understand it. Thanks!