Nikita M. Grimm
Nikita M. Grimm

Reputation: 48

How to overload a Rust function to accept closures with different argument signatures? Blanket implementation on different closure types

I am trying to implement a Rust function printf that can accept closures with different argument signatures. The function already has two implementations - printf1 which accepts closures with two i32 arguments, and printf2 which accepts closures with one i32 argument and returns a closure that also accepts an i32 argument.
(I want to have a printf function that chooses between them depending on the input or something similar)

fn printf1<F>(f: F, a: i32, b: i32)
where
F: FnOnce(i32, i32) -> i32,
{
println!("Result is {result}", result = f(a, b),)
}

fn printf2<F, G>(f: F, a: i32, b: i32)
where
F: FnOnce(i32) -> G,
G: FnOnce(i32) -> i32,
{
println!("Result is {result}", result = f(a)(b),)
}

I tried implementing a trait DynChainCall to be able to add the trait as a bound to the F type parameter of printf. However, I'm not sure how to add support for closures with two i32 arguments as well as closures that return a closure with a signature of FnOnce(i32) -> i32.

trait DynChainCall {
    fn call_print(&self, a: i32, b: i32) -> i32;
}

impl<F> DynChainCall for F
where
    F: FnOnce(i32, i32) -> i32,
{
    fn call_print(&self, a: i32, b: i32) -> i32 {
        self(a, b)
    }
}

impl<F, G> DynChainCall for F
where
    F: FnOnce(i32) -> G,
    G: FnOnce(i32) -> i32,
{
    fn call_print(&self, a: i32, b: i32) -> i32 {
        self(a)(b)
    }
}

fn printf<P: DynChainCall>(f: P, a: i32, b: i32) {
    println!("Result is {}", f.call_print(a, b));
}

-> conflicting implementations of trait DynChainCall

An example of two closures/functions that I would like to pass into printf:

fn function(a: i32, b: i32) -> i32 {
    a + b
}

fn func(a: i32) -> impl Fn(i32) -> i32 {
    move |b| a + b
}

I want printf to automatically check if the passed closure f accepts one or two arguments and call it accordingly . If the closure accepts one argument and returns a closure with a signature/closure type of FnOnce(i32) -> i32, then it should call f(a)(b). If it returns a i32 then it should call f(a, b).

printf(function, 25, 10); // This should work! ( function(25, 10) is called )
printf(func, 25, 10); // This should also work! ( func(25)(10) is called )

How can I implement this behavior of printf?

Also: I tried to somehow make a weird enum with two variants to try and do a pattern match.
But I stopped trying to implement this because it is super clunky even if it works.

enum Function {
    FnOnceVariant(Box<dyn FnOnce(i32, i32) -> i32>),
    FnVariant(Box<dyn Fn(i32) -> i32>)
}

Upvotes: 0

Views: 137

Answers (1)

Nikita M. Grimm
Nikita M. Grimm

Reputation: 48

I tried some stuff but I can't get it to work with a printf function. I will use a macro for now. (In case someone needs an alternative) This is how I did it:

  1. Make traits for both closure types
trait FunctionCall<O> {
    fn call_print(self, a: O, b: O) -> O;
}

trait FunctionComposition<O> {
    fn call_print(self, a: O, b: O) -> O;
}

2. Define my `printf` behaviour for both closures
impl<F, O> FunctionCall<O> for F
where
    F: FnOnce(O, O) -> O,
{
    fn call_print(self, a: O, b: O) -> O {
        self(a, b)
    }
}

impl<F, G, O> FunctionComposition<O> for F
where
    F: FnOnce(O) -> G,
    G: FnOnce(O) -> O,
{
    fn call_print(self, a: O, b: O) -> O {
        self(a)(b)
    }
}

3. Make a macro that expands to call `call_print`
macro_rules! printf {
    ($f:expr, $a:expr, $b:expr) => {
        println!("Result is {}", ($f).call_print($a, $b));
    };
}

4. Make two functions with the corresponding signatures
fn function(a: i32, b: i32) -> i32 {
    a + b
}

fn func(a: i32) -> impl Fn(i32) -> i32 {
    move |b| a + b
}

5. Try it out.
fn main() {
    printf!(function, 22, 33);
    printf!(func, 33, 44);
}

->
Result is 55
Result is 77

Upvotes: 0

Related Questions