4

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?

Migwell
  • 18,631
  • 21
  • 91
  • 160
  • Well, I'm curious how to solve this. Doesn't look easy though. – Netwave Nov 15 '21 at 14:35
  • Since the callback, inputs, and outputs are all "dynamic" I think you'd have to box and type-erase everything for the Vec version to work. For the other one you'd need something the Iterator composition, so the "outer" function would first call what it wraps then call itself. So you'd have `prev: FuncChain` and `func: fn(T, U)`. – Masklinn Nov 15 '21 at 14:38
  • Could you make a new type aware kind-of-linked-list where the types had to match in/out for neighbors? (still new to rust but I'm curious about the solve for this question) Edit: just re-read the question and that seems to be the suggestion. derp. – BWStearns Nov 15 '21 at 14:40
  • @Masklinn well, if that's the only way then I would love to see an example (and would accept it as the answer). But the thing is they're not exactly dynamic, it would still be possible to type check that each subsequent function can accept the previous output using an magic type checker, I'm just not sure if Rust can do it. – Migwell Nov 15 '21 at 14:41
  • @BWStearns is my second suggestion not basically a linked list? The problem is that you need to know the output type of your successor and that isn't possible. – Migwell Nov 15 '21 at 14:42
  • [Related.](https://stackoverflow.com/questions/45786955/how-to-compose-functions-in-rust) [Vaguely related.](https://stackoverflow.com/questions/66738261/higher-order-functions-supporting-possible-lifetimes) – Aiden4 Nov 15 '21 at 16:32
  • @Migwell, yes. I was pre-coffee when I initially read this. – BWStearns Nov 15 '21 at 16:36

1 Answers1

6

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.

trent
  • 25,033
  • 7
  • 51
  • 90
Masklinn
  • 34,759
  • 3
  • 38
  • 57
  • 1
    please include code inside your answer – Stargateur Nov 15 '21 at 15:08
  • [`PhantomData` is not necessary here](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f0d6bcc9eb8e070c1d9b6469f6a5e148), but even if you choose to include it it should be `PhantomData` – trent Nov 15 '21 at 15:20
  • @trentcl you can just edit the post to simplify it no? – Masklinn Nov 15 '21 at 15:25
  • Can and did. Thanks, excellent answer. – trent Nov 15 '21 at 15:56
  • You can make the solution more general by using the `Fn` trait: [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=976950b857a1ca397fdec264d07846de) – Aiden4 Nov 15 '21 at 16:23
  • @Aiden4 Think you posted the wrong playground link; that just contains the original code? – trent Nov 15 '21 at 16:31
  • @trentcl Sorry, here the is the real link (I double-checked it this time): [Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=82a4f90a976acacdcf124be93b84cb18) – Aiden4 Nov 15 '21 at 16:34
  • 2
    Here's a simple implementation of function composition as a single function: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=a24fee4afa2a13292aed0736179c21ac It's not as easy to work with, since you can't chain it as nicely, and there is no way to access the chain of functions that lives as captures inside the chain of closures. – Sven Marnach Nov 15 '21 at 16:34
  • @Aiden4 that's a rather haunted piece of code - it has more fake parameters than real ones! What you probably want here is [an associated type, similar to how the `Fn` traits themselves work](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=a45a3a8931690a311ee027699b764473). See also [why I think bounds on structs are usually wrong](https://stackoverflow.com/questions/49229332/should-trait-bounds-be-duplicated-in-struct-and-impl/66369912#66369912). – trent Nov 15 '21 at 16:59
  • (elaboration: there are no bounds on structs in Aiden's code - which is fine - but the `PhantomData` thing often comes up when trying to write such, and it shares some of the same drawbacks, which is why I thought it relevant) – trent Nov 15 '21 at 17:08
  • @trentcl you're right that's much better. I was using `PhantomData` to facilitate writing appropriate bounds, which isn't necessarily bad (see [`nalgebra::matrix`](https://docs.rs/nalgebra/0.29.0/src/nalgebra/base/matrix.rs.html#183)), but in a lot of cases, it is better to rewrite the trait. – Aiden4 Nov 15 '21 at 17:18