Handling unknown input parameter type of a closure within a macro

Hey all,

I am trying to make a macro where an indefinite number of closures can be passed, then in the macro the closures are collected in a vector.

I could come up with a solution, but I am not pleased with it. Here is what I have:

First I have a test object, for the sake of the test:

struct TestObject {
    name: String,
    age: i32
}

And here is the macro for collecting the passed closures and Boxing them:

#[macro_export]
macro_rules! actions {
    ($TStructType:ident, $($closure:expr),*)  => {
        {
            let mut temp_vec = Vec::<Box<dyn Fn(&$TStructType)>>::new();
            $(
                temp_vec.push(Box::new($closure));
            )*
            temp_vec            
        }
    };
}

And here is a unit test for the functionality:

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    pub fn test_actions_macro(){
        let closure1 = |item1: &TestObject| println!("{}",item1.name);
        let closure2 = |item2: &TestObject| println!("{}",item2.age);

        let actions: Vec<Box<dyn Fn(&TestObject)>> = actions!(TestObject, closure1, closure2);

        //Additional logic with using the actions vector
    }
}

To be honest I am not pleased with this solution because we always need to pass the type of the closure input parameter, in this case TestObject.

Is there any way to use a wildcard or so in my macro actions for TStructType? From the user experience point of view, I would be really happy to get rid of this param of this macro, so only specifying the closures.

Here is a playground link for the code.

Thanks in advance for the help!

What about coercing to Box<dyn Fn(...)> and using a _ to make inference fill in the blanks?

#[macro_export]
macro_rules! actions {
    ($TStructType:ident, $($closure:expr),*)  => {
        vec![
            $( Box::new($closure) as Box<dyn for<'a> Fn(&'a _) -> _> ),*
        ]
    };
}

(playground)

The for<'a> bit is a workaround because you can't coerce the boxed closure to Box<dyn Fn(_) -> _> because it'll infer the closure to accept references with a known lifetime instead of any lifetime.

3 Likes

Many thanks for your answer, much appreciated!

I still have one issue though.

What if I want to use this macro for simple types, and not only for structs?

So, let's say I have the following method in my production code:

pub fn apply_actions<T>(actions: Vec<Box<dyn Fn(&T)>>) {
    //some logic
}

When I have a test method like the following, everything is fine:

    #[test]
    pub fn test_actions_macro_for_struct(){
        let closure1 = |item1: &TestObject| println!("{}",item1.name);
        let closure2 = |item2: &TestObject| println!("{}",item2.age);

        apply_actions(actions!(closure1, closure2))
    }

But if I add an additional test method test_actions_macro_for_simple_type, it fails unfortunately. Here is the second test method:

    #[test]
    pub fn test_actions_macro_for_simple_type(){
        let closure1 = |item1: i32| println!("{}",item1);
        let closure2 = |item2: i32| println!("{}",item2);

        apply_actions(actions!(closure1, closure2))
    }

According to the compiler, it fails at the point when the Box::new($closure) happens in the macro. It says:

type mismatch in closure arguments

expected signature of `for<'a> fn(&'a _) -> _`

Does it mean that this workaround you proposed, works only for structs? Is it possible to overcome this and to make this work also for simple types like i32?

Here is a playground, (note that it results in the abovementioned compilation error)

Thanks again for your support!

This has nothing to do with structs vs primitives, but with references vs owned values.

The code you started with was requiring explicitly that the argument is a reference, since the type of closures being accepted is dyn Fn(&$TStructType) - note the ampersand. The same requirement was preserved in the code by Michael-F-Brian - actions are typed as dyn for<'a> Fn(&'a _) -> _. So they can't accept anything that's not a reference, such as i32.

It's harder to make the same code work for both references and non-references, due to the complexity of the lifetime inference. For example, if we just ask the compiler to infer the whole argument type, i.e. leave the action type as dyn Fn(_) -> _, it will complain that "one type is more general then the other", i.e. that the actions seems to be callable with different sets of lifetimes and so cannot have the same type. But, if we leave even more for it to infer, this would seem to work:

#[macro_export]
macro_rules! actions {
    ($($closure:expr),*)  => {
        vec![
            // no restrictions on input and output - whatever is inferred is fine
            $( Box::new($closure) as Box<dyn Fn(_) -> _> ),*
        ]
    };
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    pub fn test_actions_macro(){
        let closure1 = |item1: &TestObject| println!("{}",item1.name);
        let closure2 = |item2: &TestObject| println!("{}",item2.age);

        // also, no explicit restriction on action type - whatever is inferred is fine
        let actions: Vec<Box<_>> = actions!(closure1, closure2);
        // check that they are indeed callable
        let item = TestObject { name: "name".into(), age: 42 };
        for action in actions {
            action(&item);
        }
        
        
        let closure1 = |item1: i32| println!("{}",item1);
        let closure2 = |item2: i32| println!("{}",item2);

        // again, no explicit restriction on action type - whatever is inferred is fine
        let actions: Vec<Box<_>> = actions!(closure1, closure2);
        // check that they are indeed callable
        for action in actions {
            action(42);
        }
    }
}

Playground

2 Likes

You are totally right. My apology for expanding the scope of my original question. Next time I will provide the full code for my issue, and not only a simplified version of it.

I will accept @Michael-F-Bryan 's answer then.

But next to that, many thanks for you too for explaining the details between references and owned values! I learned a lot!

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.