Implement trait for closures

Suppose I have the following trait:

pub trait FunctionTrait<Input, Output, Context> {
    fn call(&mut self, input: &Input, context: Option<&Context>) -> Option<Output>;
}

I'd like to implement it for multiple closure types, for example like this:

impl<F, Input, Output, Context> FunctionTrait<Input, Output, Context> for F
where
    F: FnMut(&Input, Option<&Context>) -> Option<Output>,
{
    fn call(&mut self, input: &Input, context: Option<&Context>) -> Option<Output> {
        (self)(input, context)
    }
}

impl<F, Input, Output, Context> FunctionTrait<Input, Output, Context> for F
where
    F: FnMut(&Input, Option<&Context>) -> Output,
{
    fn call(&mut self, input: &Input, context: Option<&Context>) -> Option<Output> {
        Some((self)(input, context))
    }
}

But I get the following error:

error[E0119]: conflicting implementations of trait `strategy::basic::function_node::FunctionTrait<_, _, _>`

And I get it, the type F could somehow implement both traits, and then it conflicts.

So my first question is: is that even possible with these closure traits? Can something implement both FnMut() -> X and FnMut() -> Y at the same time?

My second question is: how can I do something like this?

I was looking at the axum code to see how they support functions with variable parameter quantity, and it's something similar to what I did, except that the trait itself also has the same generic parameters that are varying in the closure trait, but I couldn't think of a way to do that in my case.

Another solution I was trying was to have a wrapper type that had a call method, and could be converted from multiple closure types. But my From implementation resulted in errors:

struct FunctionWrapper<T>(T);

impl<T> FunctionWrapper<T> {
    fn call<Input, Output, Context>(
        &mut self,
        input: &Input,
        context: Option<&Context>,
    ) -> Option<Output>
    where
        T: FnMut(&Input, Option<&Context>) -> Option<Output>,
    {
        (self.0)(input, context)
    }
}

impl<Input, Output, Context, F, T> From<F> for FunctionWrapper<T>
where
    F: FnMut(&Input, Option<&Context>) -> Output,
    T: FnMut(&Input, Option<&Context>) -> Option<Output>,
{
    fn from(value: F) -> Self {
        Self(move |input, context| Some(value(input, context)))
    }
}

The error that got me puzzled the most was this:

error[E0308]: mismatched types
  --> src/strategy/basic/function_node.rs:50:14
   |
44 | impl<Input, Output, Context, F, T> From<F> for FunctionWrapper<T>
   |                                 - expected this type parameter
...
50 |         Self(move |input, context| Some(value(input, context)))
   |         ---- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected type parameter `T`, found closure
   |         |
   |         arguments to this function are incorrect
   |
   = note: expected type parameter `T`
                     found closure `{closure@src/strategy/basic/function_node.rs:50:14: 50:35}`
   = help: every closure has a distinct type and so could not always match the caller-chosen type of parameter `T`
note: tuple struct defined here
  --> src/strategy/basic/function_node.rs:29:8
   |
29 | struct FunctionWrapper<T>(T);
   |        ^^^^^^^^^^^^^^^

Which I think I understand, but how can I solve this kind of situation? Having a generic type defined by the code itself in the implementation, is that possible somehow?

Well, it's late and I'm out of ideas for now, so I'm giving up for the day!

Thanks in advance for any insight!

yes, if if implements them manually, but more importantly, rust does not look at where clauses when checking for overlapping trait impls.

generally, i try to avoid blanket trait implementations as much as possible.

I have several answers to this... the second bullet point is most pertinent to your OP though.

  • If X and Y definitely cannot be the same, and I take the signatures in this question literally, then no -- X and Y are associated types, not input parameters, so this would be implementing the same trait twice with different associated types. But you can't use that to prove disjoint implementations in Rust so far.

    • But if X and Y are generics, maybe they could be the same
    • And this is not the signature in your OP to boot
  • If we rephrase to Fn() -> X and Fn() -> Option<Y> where X and Y are generics, then yes -- X can unify with Option<Y>.

  • On nightly, you can implement for example Fn(ConcreteType) and Fn(Option<ConcreteType>) for the same implementing type

    • I don't think anything does this on stable yet... but with non-lifetime binders, it may be possible in the future, so it probably wouldn't be allowed even if there are no examples on stable today
  • On stable, things do implement Fn(&'lifetime1 ConcreteType) and Fn(&'lifetime2 ConcreteType) though, and so when we get into generic inputs, you can again demonstrate cases of actual overlap on stable today. str::trim implements Fn(&'a str) -> &'a str for every lifetime 'a for example.

    • Your implementations have generic inputs, but we've already established that they overlap without needing to explore this further

Here's a way but as the playground shows, it's not friendly to inference. Maybe there's a way with a custom trait for the return type instead... I didn't try. Being this generic on closures does tend to wreck inference in my experience.

impl<F, Input, Intermediate, Output, Context> FunctionTrait<Input, Output, Context> for F
where
    F: FnMut(&Input, Option<&Context>) -> Intermediate,
    Intermediate: Into<Option<Output>>,
{
    fn call(&mut self, input: &Input, context: Option<&Context>) -> Option<Output> {
        (self)(input, context).into()
    }
}

It's the same reason this fails (but on an implementation level instead of function level).

fn foo<CallerChooses: Default>() -> CallerChooses {
    // I said I can return any `CallerChooses: Default`
    // but instead I only return one specific type
    0
}

For the other errors, you need to get the generics of the implementation into parameters of the implementing type (or trait, if there was a trait present).

struct FunctionWrapper<Input, Context, Output, F> {
    callable: F,
    _params: PhantomData<fn(&Input, Option<&Context>) -> Option<Output>>,
}

impl<Input, Context, Output, F> FunctionWrapper<Input, Context, Output, F>
where
    F: FnMut(&Input, Option<&Context>) -> Option<Output>,
{
    // Note how the `F`s match up this time
    fn from(callable: F) -> Self {
        Self { callable, _params: PhantomData }
    }

    fn call(..)
}

Then you can add a different constructor for non-Option returning functions.

// We use a concrete type for `F` here because otherwise the compiler will
// try and fail to infer one
impl<Input, Context, Output> FunctionWrapper<Input, Context, Output, ()> {
    fn from_unwrapped<Other>(mut other: Other)
    ->
        FunctionWrapper<
            Input,
            Context,
            Output,
            impl FnMut(&Input, Option<&Context>) -> Option<Output>,
        >
    where
        Other: FnMut(&Input, Option<&Context>) -> Output,
    {
        FunctionWrapper {
            // Ugly due to Rust's horrible closure inference
            callable: move |input: &_, context: Option<&_>| Some(other(input, context)),
            _params: PhantomData
        }
    }
}

Note how we used -> .. impl FnMut(..) instead of a generic. That means you, the method writer, get to choose the type. Which happens to be unnameable.

1 Like

you are saying that you can convert to a FunctionWrapper that wraps around any callable type, however you are only converting into one specific callable type. rust does have existential types (representing a single unnamed typed, like a closure), unfortunately i don't think they can be used in this position.

Iā€™m not sure this is quite true: In @felipouā€™s trait definition, Output is a type parameter rather than an associated type, so the two impls can coexist for the same object, as demonstrated if you implement for fn() instead of closuresā€” I think this really falls into your first case of not being able to prove disjointness via associated types.

1 Like

Ah, good point, they are different trait implementations after resolving the input types. I guess Output in the trait parameter list is what I was calling Intermediate. So my single implementation isn't the same thing (but trying to make it the same thing results in an unconstrained parameter).

And I'll probably wait for the OP to respond before poking any more.

1 Like

Thanks a lot for all the answers, specially @quinedot!

One thing that really surprised me, that I didn't know (not sure if I forgot or just never knew), is that the output in these Fn traits is an associated type! I thought everything was a type parameter! That alone was a great insight.

@quinedot I used your suggestion, and now I was able to do what I wanted! Thanks a lot!

Here's the final solution: the trait, and 3 impls for differente types of FnMuts, with different lists of arguments:

pub trait FunctionTrait<Input, Output, Context, Args> {
    fn call(&mut self, input: &Input, context: Option<&Context>) -> Option<Output>;
}

impl<F, Input, Intermediate, Output, Context>
    FunctionTrait<Input, Output, Context, (Input, Context)> for F
where
    F: FnMut(&Input, Option<&Context>) -> Intermediate,
    Intermediate: Into<Option<Output>>,
{
    fn call(&mut self, input: &Input, context: Option<&Context>) -> Option<Output> {
        (self)(input, context).into()
    }
}

impl<F, Input, Intermediate, Output, Context> FunctionTrait<Input, Output, Context, (Input,)> for F
where
    F: FnMut(&Input) -> Intermediate,
    Intermediate: Into<Option<Output>>,
{
    fn call(&mut self, input: &Input, _context: Option<&Context>) -> Option<Output> {
        (self)(input).into()
    }
}

impl<F, Input, Intermediate, Output, Context> FunctionTrait<Input, Output, Context, ()> for F
where
    F: FnMut() -> Intermediate,
    Intermediate: Into<Option<Output>>,
{
    fn call(&mut self, _input: &Input, _context: Option<&Context>) -> Option<Output> {
        (self)().into()
    }
}

Lack of type inference in some places is indeed a little bit annoying, I'll try to experiment with a custom trait like you said, but I can bear it for now.

What is really bothering me right now is that "Args" type parameter that I had to add to FunctionTrait in order to make it accept different implementations. I can understand why it is currently needed, but is there someway around that?

The problem is mostly that whenever I have a closure that I want to return as an "impl FunctionTrait" I have to specify the Args correctly, and it would be better to hide that as an implementation detail.

Hmm... does this work for you?

  • I added a default type for the Args parameter (Args = ())[1]

  • And added a newtype that wraps arbitrary F: FunctionTrait<..> types

    struct EraseArgs<F, Args>(F, PhantomData<Args>);
    impl<F, Args> EraseArgs<F, Args> {
        fn new<Input, Output, Context>(f: F) -> Self
        where
            F: FunctionTrait<Input, Output, Context, Args>,
        {
            Self(f, PhantomData)
        }
    }
    
  • And made it implement with Args = (), the default, by dispatching to FunctionTrait::call (since that call doesn't care what Args is)

    impl<F, Input, Output, Context, Args> 
        FunctionTrait<Input, Output, Context> for EraseArgs<F, Args>
    where
        F: FunctionTrait<Input, Output, Context, Args>,
    {
        fn call(&mut self, input: &Input, context: Option<&Context>) -> Option<Output> {
            self.0.call(input, context)
        }
    }
    
  • And you can use it to wrap your opaque returns into something with Args = (), no matter what the Args type was on the wrapped F

    pub fn hmm() -> impl FunctionTrait<String, i32, f32> {
        EraseArgs::new(
            |_: &String, _: Option<&f32>| -> i32 { 0 }
        )
        /*
        EraseArgs::new::<_, _, f32>(
            |_: &String| -> i32 { 0 }
        )
        */
        /*
        EraseArgs::new::<String, _, f32>(
            || -> i32 { 0 }
        )
        */
        /*
        || -> i32 { 0 }
        */
    }
    

This does add a layer of indirection, which hopefully optimizes out most of the time, but may not always. And has the same inference hurdles sometimes, as indicated by the commented examples.


  1. It doesn't have to be () if something else makes more sense, e.g. corresponds to the most common case ā†©ļøŽ

Wow, this definitely helps! Thanks a lot!

I think I'm beginning to see why people say that C++ meta-programming can get so complex! :laughing:

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.