Search code examples
rustmethod-chaining

Chaining methods in Rust


I have recently tried to create a simple builder pattern that allows for chaining methods together.

The API for it should look similar to this:

let chained = MethodChainer::new()
                  .then(method1)
                  .then(method2)
                  .then(method3);

chained.run(x); // Will be equivalent to `method3(method2(method1(x)))`

However, I'm struggling to come up with a good implementation.

One option, is to store the chained method and when then is called, swap the chained method with the following:

fn then(self, new_method: /*some method type*/){
    self.chain = |x| {new_method(self.chain(x))}
}

Or something similar. But this approach limits controlling how the functions are called and how I can intervene in the middle (e.g, want to allow a "debug mode" that when activated, it will print each result from each method that was chained).

Optimally, for me - the "best" implementation would put all chained methods into one Vec, and then running them will loop over that Vec and execute each method. But this is not possible in safe rust due to type restrictions, and I'm not sure if its even possible to implement it even in unsafe rust...

That is why I'm asking here - is this feature possible to implement (safe or unsafe rust)? If so - then how?


Solution

  • The vector approach is a bit tricky, because you would need to be generic about the intermediate types of your chaining. This, in turn, would require a different number of generic parameters, depending on how often then is called - so the type needs to change on every call of then. We cannot list all of the types in a long list, as that would require variadic generics and Rust doesn't have that. You can however use a generic type to nest your chained methods, i.e. keep the old instance in a field of the new instance. One problem that occurs is the implementation of run - we would to compare the inner value against some maker to notice that we are in the base-case, i.e. there are no more nested methods. To get around that, one can use a helper trait Run and a marker type that indicates "no more nested methods" and can then have its own implementation of the Run trait. If Rust ever gets specialization in stable, we could probably use () as a marker type instead and get rid of the Run trait. Anyways, here is what I came up with:

    use std::marker::PhantomData;
    
    struct BaseChainer;
    
    struct MethodChainer<X, Y, F, I> {
        _x: PhantomData<X>,
        _y: PhantomData<Y>,
        f: F,
        inner: I,
    }
    
    impl<X> MethodChainer<X, X, fn(X) -> X, BaseChainer> {
        pub fn new() -> Self {
            Self {
                _x: PhantomData,
                _y: PhantomData,
                f: |x| x,
                inner: BaseChainer,
            }
        }
    }
    
    impl<X, Y, F, I> MethodChainer<X, Y, F, I> {
        pub fn then<G, Z>(self, new_method: G) -> MethodChainer<X, Z, G, MethodChainer<X, Y, F, I>>
        where
            F: Fn(X) -> Y,
            G: Fn(Y) -> Z,
        {
            MethodChainer {
                _x: PhantomData,
                _y: PhantomData,
                f: new_method,
                inner: self,
            }
        }
    }
    
    trait Run<X> {
        type Y;
    
        fn run(&self, x: X) -> Self::Y;
    }
    
    impl<X> Run<X> for BaseChainer {
        type Y = X;
    
        fn run(&self, x: X) -> X {
            x
        }
    }
    
    impl<X, Y, Z, F, I> Run<X> for MethodChainer<X, Z, F, I>
    where
        F: Fn(Y) -> Z,
        I: Run<X, Y = Y>,
    {
        type Y = Z;
    
        fn run(&self, x: X) -> Z {
            (self.f)(self.inner.run(x))
        }
    }
    
    fn main() {
        let plus_one = |x: i32| x + 1;
        let chained = MethodChainer::new()
            .then(plus_one)
            .then(|x| x.to_string())
            .then(|s| s + " - wow!");
        println!("{}", chained.run(5));
    }
    

    It could probably be shorter, but should get you started.


    Note that this forces you to either make all functions taking a MethodChainer to be generic over its F and I generic parameters.