rustrust-macros

How to avoid conflicting implementation issue in a Rust macro involving invocations for different arities?


I have the following trait, and would like it to work for any Fn.

pub trait CloneableFn<Args, O>: Fn(Args) -> O {
    fn clone_box<'a>(&self) -> Box<dyn 'a + CloneableFn<Args, O>>
    where
        Self: 'a;
}

After trying various approaches, I'm now trying to use a macro.

macro_rules! cloneable_fn {
    ($($arg:ident),* $ (,)?) => {
        impl<$($arg,)* O, FN: Fn(($($arg),*)) -> O + Clone> CloneableFn<($($arg),*) , O> for FN
        {
            fn clone_box<'a>(&self) -> Box<dyn 'a + CloneableFn<($($arg),*), O>>
            where
                Self: 'a,
            {
                Box::new(self.clone())
            }
        }
    };
}

cloneable_fn!();
// cloneable_fn!(A);
cloneable_fn!(A, B);
cloneable_fn!(A, B, C);
cloneable_fn!(A, B, C, D);
cloneable_fn!(A, B, C, D, E);
cloneable_fn!(A, B, C, D, E, F);
cloneable_fn!(A, B, C, D, E, F, G);

This appears to compile, but if I uncomment the line cloneable_fn!(A);, there is an error:

error[E0119]: conflicting implementations of trait `util::CloneableFn<(), _>`
  --> src/util.rs:52:9
   |
52 |         impl<$($arg,)* O, FN: Fn(($($arg),*)) -> O + Clone> CloneableFn<($($arg),*) , O> for FN
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |         |
   |         first implementation here
   |         conflicting implementation
...
65 | cloneable_fn!(A);
   | ---------------- in this macro invocation
   |
   = note: this error originates in the macro `cloneable_fn` (in Nightly builds, run with -Z macro-backtrace for more info)

At first, I thought it had to do with a parentheses issue, but the issue remained I left cloneable_fn!(A); commented and manually added an impl:

impl<A, B, F: Fn(A) -> B + Clone> CloneableFn<A, B> for F {
    fn clone_box<'a>(&self) -> Box<dyn 'a + CloneableFn<A, B>>
    where
        Self: 'a,
    {
        Box::new(self.clone())
    }
}

Is there a solution to this approach that would work with any arity (it's fine if the solution involves some special case implementations)?


Solution

  • The issue boils down to a syntax quirk: (A) is not interpreted as a 1-ary tuple of a generic type A, but as just A, and thus the implementation for ClonableFn<A, O> conflicts with every other ClonableFn implementation, as (), (A, B), ... are types.

    This is also why your manual implementation fails: you attempt to write ClonableFn<A, O> which just conflicts with the world.

    The implementation you want -- for unary tuple -- needs to be written for CloneableFn<(A,), O>.

    You could, of course, write it manually... but it can indeed be achieved by "fixing" your macro: depending on whether the comma is placed inside or outside the parentheses, $()* (and $()+) behave differently:

    Using the Playground, and clicking on "Tools > Expand Macros" we can see the difference in action.

    Your version (outside):

    #[allow(unused_parens)]
    impl<O, FN: Fn(()) -> O + Clone> CloneableFn<(), O> for FN {
        fn clone_box<'a>(&self) -> Box<dyn 'a + CloneableFn<(), O>> where
            Self: 'a {
            Box::new(self.clone())
        }
    }
    #[allow(unused_parens)]
    impl<A, O, FN: Fn((A)) -> O + Clone> CloneableFn<(A), O> for FN {
        fn clone_box<'a>(&self) -> Box<dyn 'a + CloneableFn<(A), O>> where
            Self: 'a {
            Box::new(self.clone())
        }
    }
    #[allow(unused_parens)]
    impl<A, B, O, FN: Fn((A, B)) -> O + Clone> CloneableFn<(A, B), O> for FN {
        fn clone_box<'a>(&self) -> Box<dyn 'a + CloneableFn<(A, B), O>> where
            Self: 'a {
            Box::new(self.clone())
        }
    }
    

    Fixed version (inside):

    #[allow(unused_parens)]
    impl<O, FN: Fn(()) -> O + Clone> CloneableFn<(), O> for FN {
        fn clone_box<'a>(&self) -> Box<dyn 'a + CloneableFn<(), O>> where
            Self: 'a {
            Box::new(self.clone())
        }
    }
    #[allow(unused_parens)]
    impl<A, O, FN: Fn((A,)) -> O + Clone> CloneableFn<(A,), O> for FN {
        fn clone_box<'a>(&self) -> Box<dyn 'a + CloneableFn<(A,), O>> where
            Self: 'a {
            Box::new(self.clone())
        }
    }
    #[allow(unused_parens)]
    impl<A, B, O, FN: Fn((A, B)) -> O + Clone> CloneableFn<(A, B), O> for FN {
        fn clone_box<'a>(&self) -> Box<dyn 'a + CloneableFn<(A, B), O>> where
            Self: 'a {
            Box::new(self.clone())
        }
    }
    

    The trailing comma in (A,) makes all the difference.

    1 Wait, you say, there's no trailing comma in (A, B)! You are correct, and I am puzzled. Strictly speaking it's unnecessary, but even cargo expand --ugly lacks it, even though it should be strictly expanded code, with no reformatting. I'll update this answer once I understand why.