Method chaining special form


#1

A few methods have slipped in that, as far as I can tell, are just to make method chaining more convenient. In particular, Vec.append_one is just a push that returns self afterwards. Rather than bloat up the API with lots of similar-but-subtly-different methods, I’d prefer if this pattern were generalized to something reusable.

Ruby has tap for exactly this, and tap could be implemented pretty easily in Rust, though it’s exceptionally verbose:

//Stolen from kimundi

trait Tap { 
    fn tap(mut self, f: |&mut Self|) -> Self { 
        f(&mut self); 
        self 
    } 
} 

impl<T> Tap for T {} 

fn main () {
    let v = vec![1u, 2, 3]; 
    v.tap(|v| v.push(100)).tap(|v| v.push(1024));
}

(on play)

An actual special form like a .. or -> operator would probably be more friendly, although Mo’ Sigils Means Mo’ Problems.

Regardless, it would just be a trivial sugar for foo.bar(); foo, and usage would look like:

foo..push(1)..push(2)..push(3)
foo->push(1)->push(2)->push(3)
foo(╯°□°)╯︵push(1)(╯°□°)╯︵push(2)(╯°□°)╯︵push(3)

#3

could a macro handle this


#4

A bit of sugar here goes a long way. Dart has the .. operator. I’d love to see something like this in Rust.


#5

One reason I would prefer not to use .. for this is that I had an idea about making a..b sugar for range(a, b), which would be usable for things like v[a..b], without requiring a special slicing operator, just range sugar and overloaded Index.


#6

It is possible with a macro if you don’t mind the outer invocation syntax:

macro_rules! meth_chain(
    ($inp:expr $(-> $call:ident($($e:expr),*))*) => (
        {
            let mut tmp = $inp;
            $(
                tmp.$call($($e),*);
            )*
            tmp
        }
    );
    ($inp:expr $(-> $call:ident::<$($t:ty),*>($($e:expr),*))*) => (
        {
            let mut tmp = $inp;
            $(
                tmp.$call::<$($t),*>($($e),*);
            )*
            tmp
        }
    )
)

fn main() {
    let a = vec![1u, 2, 3];
    let b = meth_chain!(a->push(50));
    let c = meth_chain!(
        b->push(50)
         ->push(100)
         ->push(150)
    );

    assert_eq!(c, vec![1, 2, 3, 50, 50, 100, 150])
}

playpen


#7

Is there any reason for push itself, and similar methods, not to return &mut Self?


#8

HKT + monads == done :wink:


#9

@glaebhoerl: That leads to these problems:

  1. The library writer always has to keep in mind all possible conventions for chaining, which will lead to inconsistency because he will forget or not bother.
  2. Methods no longer do just their thing, they do their thing + something unrelated that is only useful in comparatively rare cases.
  3. Its not possible for all methods that might need this. For example, remove() on a set would return a bool indicating whether the element was actually in there, which would be incompatible with this scheme

#10

The idea was that this would be an easy 80% solution that could be achieved merely through convention. The motivating thought is, essentially: why are we trying to come up with technical workarounds for our modifier-methods not supporting chaining, instead of making them support chaining?

Basically we would have the convention that modifier methods taking &mut self shouldn’t return (), they should return a useful result instead, namely &mut Self. This addresses your first point: conventions are inherently things library writers need to keep in mind, and this would be a fairly simple one. (We could even have a lint, if we really wanted to.) This wouldn’t solve every case, for instance it wouldn’t work for methods like remove() which return additional information, but it would work for quite a lot of them, and would remove most of the pain at no cost to the language itself.


#11

There’s some pseudo-back-compat issues here. If I have a method that returns nothing, I don’t think many people will have a lot of trouble if I start making it return something. Recompiles will of course be necessary, and an extra semicolon might need to be added here or there, but client code shouldn’t be very disrupted. However, if I have a method that returns self because “why not”, and then I want to start returning something meaningful, then that’s a much more serious change.

As an example, there’s some leaning towards having little-to-no collection traits circa 1.0, because generic collections simply won’t work right at that point in time. So maybe Vec and whoever have their own push operation that always succeeds, but some collections have a push that potentially isn’t as clean (e.g. fixed-size collections) and thus returns something relevant. Eventually we decide to introduce some unifying traits and, oops, we had Vec.push returning self “because we could”.

More generally, though, a uniform convention/syntax just seems desirable for programmer sanity.


#12

@Gankro Good points. It should be noted though that Vec wouldn’t need to lose its own push just because it later ended up also implementing some unifying trait. If the signatures of the pushes differred, the result would be some inconsistency, but not a backwards compatibility problem per se.


#13

+1 for .. syntax which Dart uses. Its very nice and simple, and improves readability immensely.


#14

-1 for .. - +1 for keeping .. free for ranges, like swift, and analogous to the [T…N] -1 for -> - it will confuse the hell out of a C++ conditioned mind… keep it for functions .