How can we store a chain of heterogeneous functions in Rust?

Viewed 144

I'm working on some code where I'm interested in a lazy-evaluated function chain. In other words, it stores all the operations you want, and only evaluates them all together.

This is very easy when all the functions in the chain take the same type and return the same type. However, I'm stuck on how to make this work when the chain of functions returns a different type each time. This easy case can be seen in the following code:

struct FuncChain<T> {
    funcs: Vec<fn(T) -> T>
}

impl<T> FuncChain<T> {
    fn call(&self, input: T) -> T {
        self.funcs.iter().fold(input, |prev, func| func(prev))
    }
}

fn main(){
    let fc = FuncChain {
        funcs: vec![
            |x| x + 1,  
            |x| x + 2,
            |x| x * 2,
            |x| x - 2,
        ]
    };
    println!("{}", fc.call(1));
}

(Playground)

So in this case we go i32 -> i32 -> i32 -> i32 -> i32.


What I want to do is a more general case where we go A -> B -> C -> D -> E, meaning that the funcs vector contains: fn(A) -> B, fn(B) -> C, fn(C) -> D, and fn(D) -> E. But how can this type definition be assigned to a struct? I can't create a vector with heterogeneous types, and even if I could, what would the type signature of the struct be?

I could make a recursive type definition perhaps, where the FuncChain holds a pointer to the first function object, and also the next object in the chain :

struct FuncChain<S, T, U> {
    func: fn(S) -> T,
    next: FuncChain<T, U, ?>
}

impl<S, T, U> FuncChain<S, T, U> {
    fn call(&self, input: T) -> T {
        self.funcs.iter().fold(input, |prev, func| func(prev))
    }
}

fn main(){
    let fc = FuncChain {
        funcs: vec![
            |x| x.toString(),  
            |x| u8::fromStr(x),
            |x| x.toString(),
            |x| i32::fromStr(x),
        ]
    };
    println!("{}", fc.call(1));
}

However of course this won't work, because I can't know the output type of next.

How can this be done?

1 Answers

You question is similar to Iterator, and so can be solved the same solution: a trait indicating a "callable".

The trait lets you "break" the infinite recursion of your current struct-based system, by having the struct just denote it as "whatever that does".

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f0d6bcc9eb8e070c1d9b6469f6a5e148

struct Chain<U, V, F> {
    prev: F,
    f: fn(U) -> V,
}

trait FuncChain<T, U> {
    fn call(&self, _: T) -> U;
    fn chain<V>(self, next: fn(U) -> V) -> Chain<U, V, Self>
    where
        Self: Sized,
    {
        Chain {
            prev: self,
            f: next,
        }
    }
}

impl<T, U> FuncChain<T, U> for fn(T) -> U {
    fn call(&self, t: T) -> U {
        self(t)
    }
}

impl<T, U, V, F> FuncChain<T, V> for Chain<U, V, F>
where
    F: FuncChain<T, U>,
{
    fn call(&self, t: T) -> V {
        (self.f)(self.prev.call(t))
    }
}

fn main() {
    let c = ((|x| x + 1) as fn(i32) -> i32)
        .chain(|x| x * 2)
        .chain(|x| x - 2);
    println!("{}", c.call(5));
}

A better Rustacean can probably design a simpler way to achieve this.

If you're fine with using nightly, there's probably a way to use Fn instead of needing a custom trait.

Hell, fundamentally it's just . so you can probably manage with just a generic function and a closure, I'll have to check.

Related