Search code examples
rustfunction-pointerstraits

implementing traits for dyn Fns


Today I was playing around with function traits. Though the example I show below might not practically be very useful, I do wonder why it doesn't compile.

pub fn do_something(o: &(dyn Other + 'static)) {

}

trait Other {
    fn do_something_other(&self);
}

impl<A> Other for dyn Fn(A) {
    fn do_something_other(&self) {
        do_something(self);
    }
}

Here I implement a trait for a function type. This function type is generic over it's parameter. This means that if you were to do it like this:

pub fn do_something(o: &(dyn Other + 'static)) {

}

trait Other {
    fn do_something_other(&self);
}

impl<F, A> Other for F where F: (Fn(A)) + 'static {
    fn do_something_other(&self) {
        do_something(self);
    }
}

you get an error stating a type parameter is unconstrained. error1

I get this and don't believe it's possible to do it with generics. But the dynamic approach, why doesn't it work? It gives the following error:

error2

I don't understand this error. It states I pass a Fn(A) -> (), which doesn't implement Other. However, this error occurs literally in the implementation of Other. How can it not be implemented here?

My first thought was because each closure is its own type. If it has to do with this, I find the error very weird.


Solution

  • The first construction fails because you cannot convert a &dyn A into a &dyn B, even when implementing B for dyn A.

    trait A {}
    
    trait B {
        fn do_thing(&self);
    }
    
    impl B for dyn A {
        fn do_thing(&self) {
            let b: &dyn B = self;
        }
    }
    
    error[E0308]: mismatched types
     --> src/lib.rs:9:25
      |
    9 |         let b: &dyn B = self;
      |                ------   ^^^^ expected trait `B`, found trait `A`
      |                |
      |                expected due to this
      |
      = note: expected reference `&dyn B`
                 found reference `&(dyn A + 'static)`
    

    Well, you can convert traits but only with help from the source trait. But since in this case the source is Fn, that's not a route.


    The second construction fails because Rust won't let you implement traits that can conflict. Trying to implement B for a type that implements A<_> will automatically be rejected because types can have multiple implementations of A<_>.

    trait A<T> {}
    
    trait B {
        fn do_thing(&self);
    }
    
    impl<T, U> B for T where T: A<U> {
        fn do_thing(&self) {}
    }
    
    error[E0207]: the type parameter `U` is not constrained by the impl trait, self type, or predicates
     --> src/lib.rs:7:9
      |
    7 | impl<T, U> B for T where T: A<U> {
      |         ^ unconstrained type parameter
    

    Regarding Fns in particular, its somewhat hard to tell since usually function objects only implement a single Fn trait. However, the keyword is usually since you can enable a feature on nightly to do just that. And the trait system usually doesn't play favorites.


    So what can you do? Well the first method is still functional, just you have to keep the implementation within the trait. You can use the second method if you use a concrete types for the function arguments.

    You can conceivably implement Other for &dyn Fn(_) (implementing it on the reference and not the object itself). But that's not particularly convenient with how Fn objects are usually used.

    pub fn do_something(o: &dyn Other) {}
    
    trait Other {
        fn do_something_other(&self);
    }
    
    impl<A> Other for &dyn Fn(A) {
        fn do_something_other(&self) {
            do_something(self);
        }
    }
    
    fn main() {
        // THIS WORKS
        let closure: &dyn Fn(_) = &|x: i32| println!("x: {}", x);
        closure.do_something_other();
        
        // THIS DOESN'T WORK
        // let closure = |x: i32| println!("x: {}", x);
        // closure.do_something_other();
    }
    

    Another option would be to make the Other trait generic in order to constrain A, but that of course depends on how its designed to be used.