Chaining trait functions with associated types together

Hi!

I'm implementing a pipeline to process numeric values (each operation returns different types) and I'm wondering what the best way would be to chain the operations together dynamically.

With dynamically I mean that I would like a behaviour similar to conditionally pushing the operations as boxed dynamic traits into a vector and calling them successively, with each return value being passed as the input value to the next operation. This specific approach won't work with the type constraints for the Vec<Box<dyn Operation>>.
The method of returning impls / builder pattern seems also like a viable approach, but I'm not sure how I can retain the possibility of building a pipeline, i.e. chain of operations, dynamically with this (besides it not being applicable in the code snippet below since impl return values are not allowed inside the trait implementations).

I made a simplified example to show what I'm trying to "chain together":

pub trait Operation {
    type Input;
    type Output;

    fn run(&mut self, input: Self::Input) -> Self::Output;
}

struct Operation1;
struct Operation2;
struct Operation3;

impl Operation for Operation1 {
    type Input = i32;
    type Output = u32;

    fn run(&mut self, input: Self::Input) -> Self::Output {
	input as u32
    }
}

impl Operation for Operation2 {
    type Input = u32;
    type Output = u64;

    fn run(&mut self, input: Self::Input) -> Self::Output {
	input as u64
    }
}


impl Operation for Operation3 {
    type Input = u64;
    type Output = i32;

    fn run(&mut self, input: Self::Input) -> Self::Output {
	input as i32
    }
}

Do I need some macro magic for this or is it possible otherwise, e.g. with closures instead of trait functions to implement the operations?
I'd be grateful for any pointers :slight_smile:

You won't be able to do this if you don't specify the associated type. This type must be same for all entries in a vector, since it is a part of the trait object type.

You're right, thanks for pointing this out! I already encountered this problem and phrased it badly in my initial post: I'd like a similar dynamic behaviour to pushing boxed traits in a vec, but it obviously won't work in this specific way. Will add that to my original question.

Depending on our needs, you can create a wrapper that chains together two operations like this:

struct Chained<A, B> { a: A, b: B }

impl<A, B> Operation for Chained<A, B>
where
    A: Operation,
    B: Operation<Input=A::Output>,
{
    type Input = A::Input;
    type Output = B::Output;
    
    fn run(&mut self, input: Self::Input) -> Self::Output {
        self.b.run(self.a.run(input))
    }
}

You can then add a .chain() method for convenience, either as part of the same trait or an extension trait:

fn chain<B>(self, b: B) -> Chained<Self, B>
where
    Self: Sized,
    B: Operation<Input=Self::Output>
{
    Chained { a: self, b }
}

Then you can chain operations like this:

Operation1.chain(Operation2).chain(Operation3);

Complete example in playground.

The downside to this approach is that the type of the whole chain must be known at compile time; it's not a good fit if you need to swap items in and out of the chain dynamically.

Thanks! I think that's what I'll be going with. It's really nice with the compile-time typing of the chain and without dynamic dispatching. The dynamic behaviour I need is limited to turning on/off single operations in the chain, so I can imagine building the entire chain with your method, toggling single operations with a wrapper or passing an on/off flag during operation initialization will work well.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.