Pass a closure as a trait argument

I trying to define a function that can take either a closure or a str and then can print it, my understanding is that I need to define a trait and then implement it for both types but I cannot get it to work for the case taking a closure. Here is what I have tried

pub trait ParamDisplay {
    fn to_string(&self) -> String;
}

impl ParamDisplay for dyn std::ops::Fn() -> String {
    fn to_string(&self) -> String {
        self()
    }
}

impl ParamDisplay for str {
    fn to_string(&self) -> String {
        self.to_owned()
    }
}

fn print_block<'a, T: ParamDisplay + ?Sized>(block: &'a T) {
    println!("block: {}", block.to_string())
}

fn main() {
    print_block("first block");
    let x = 5;
    print_block(|| format!("param {}", x));
}

There are a few reasons why it doesn't compile as-is.

First, the trait requires that the method takes a reference to self, but in the closure case, you're not passing a reference. We can try just adding a reference...

But print_block(&|| ...) also doesn't work. The error is talking about the trait not being implemeted for some specific closure. We'll return to here, but for now let's try casting.

&(|| ...) as &dyn Fn() -> String still doesn't work. Now the compiler is complaining that it isn't 'static. The problem this time is that dyn Trait types always have a special lifetime, a "lifetime of applicability" if you will. You can almost always not write it out, but the default behavior you get when you elide it is very context sensitive and not always what you want. In this case, your implementation of the trait was only for dyn Fn() -> String + 'static, for example.

So you can implement it for any lifetime instead, and now things work.

impl ParamDisplay for dyn std::ops::Fn() -> String + '_ {
//                                                 ^^^^

However, it's pretty unwieldly to call. Being generic in your implementation can relieve some of the pain though: dyn Fn() -> String + '_ is but a subset of all types that implement Fn() -> String. If you change your implementation to be generic over types:

impl<F: Fn() -> String> ParamDisplay for F {

Then there is no implicit 'static bound and you don't need to cast to a dyn Fn() -> String + '_ specifically. So now our first attempt at the callsite works:

    print_block(&|| format!("param {}", x));

What if you wanted to get rid of explicitly taking a reference altogether? str and dyn Trait are unsized, so you can't pass them by value. But you can redefine the trait to take self instead, and implement it for &str and (generic) closures not behind a reference.

The ergonomics at the call site become much better, but you have lost some functionality:

  • ParamDisplay is no longer dyn-safe, and perhaps you need it to be
  • You'll need a + Copy or perhaps Clone bound if you want to still be able to copy around generic implementing types the way you could shared references

One more approach, sort of half-way between, is to keep the parameter as &self, but change print_block to take by value, and add an implementation for &str.

Ultimately, which approach is best will depend on your use case.

4 Likes

How would you go above putting ParamDisplay objects in a list?

You'll need the parameter to be &self instead of self so that dyn ParamDisplay is meaningful. Then you'll need to type erase the objects to either &dyn ParamDisplay or Box<dyn ParamDisplay + '_>.

Example. As you can perhaps perceive, it may be tricky to maneuver the lifetime requirements of storing a bunch of non-'static trait objects though. If you could just store a Vec<String> or the like, I'd suggest doing so (but recognize this minimized example may not reflect your actual use case).

1 Like