[Blog Post] Chaining Functions Without Returning Self

#1

tl;dr: Don’t return self at the end of a method if you’re only doing so to enable method chaining!

Let me know what you think :grin: I spent way to much time digging into this and trying to demonstrate thoroughly that returning self isn’t an ergonomic way of implementing method chaining. Along the way I discovered cascade which seems like a promising alternative. If you know of any other alternatives, please let me know!

8 Likes

#2

Excellent diagnosis of the problem. The builder pattern has unfortunate trade offs and awkward cases.

I’ve seen several syntaxes for chaining/piping (e.g. |> seems to be a popular choice) …but they all seem strange to me.

0 Likes

#3

Informative post! My takeaway is that when we apply a method we may care about two different things for different purposes:

  1. the status of the object after the modification made by the method
  2. the return value of that method

The native method chaining focuses on 1 and forces to return self. This post notices that when people already defined a method with particular return value instead of self, such method may not be able to be used in method chaining.

I do like the idea in the final section about cascade. That makes me to be reminiscent of %>% pipes in R programming language. That is also not a native language feature in R but provided by some library (aka package) like magrittr. R provides such useful mechanism for library developers so that the usage of pipe operators is just like a native one.

In rust, macro is a powerful weapon to extend the language but I haven’t seen any usage of macros as convenient as native language feature. If we can finally push cascade to be a native syntax, that’ll be great. Let’s gather more comments from the community, I believe such practical feature will benefit many domains (e.g. data science from my mention of R pipe).

I strongly encourage to raise a RFC on this feature. Maybe we can start with a pre-RFC on internals forum or design channel on discord to get feedbacks from core team and other professional users.

0 Likes

#4

Personally think more syntax as cascade just makes the language harder.
It reminds me of JavaScript “with”.

2 Likes

#5

So giving a value a name in order to use some chain_ref is considered friction:

let mut command = Command::new("foo");
command.arg("--bar");

if set_baz {
    command.arg("--baz");
}

let result = command
    .arg("quux")
    .status()
    .unwrap();

but doing the same using an ugly macro call is not:

let foo = cascade! {
    command: Command::new("meh");
    ..arg("--bar");
    // What is that anyway, an unfinished lambda parameter list, an `or` pattern, half a lambda?..
    | if set_baz { 
        cascade! {
            &mut command;
            ..arg("--baz");
        }
    };
    ..arg("quux");
    ..status();
    ..unwrap();
};

You cannot be serious. Explained all the problems of the classical solution in detail only to propose this hack as a solution.

  • It tries to fix a minor issue (not even an issue, IMHO) with a complex tool; shooting a bird with an artillery strike comes to mind: you may hit two birds at once, but is it really worth it?
  • It needlessly introduces new syntax for what is already there in the language
  • The syntax is not intuitive at all:
    • Uses the range operator .. for a method call
    • Even if you do not know what .. does in normal Rust code, it is still not obvious even from common sense
  • Abundance of punctuation makes it look noisy
  • The last call in the chain is still terminated with ;, even though it does return a value; this is confusing

Your main concern is that returning self is not semantically obvious, but what you ended up with is even less obvious and confusing in addition to that. Method chaining requires you to return something to call methods on, so you gotta play by the rules. If returning self is not self-explanatory, go invent some structures that can be returned instead. If you still cannot come up with something usable, just do not use method chaining at all. After all, you said returning self does not make sense here, so do not return anything:

let mut command = Command::new("foo");
command.add_arg("--bar");
if set_baz {
    command.add_arg("--baz");
}
command.add_arg("--quux");
if let Err(e) = command.status() {
    ...
}

This post is on TWIR, so it has huge impact on the Rust community, and I do not want Rust to become another ugly beast like C++. Sorry if you find my tone too harsh, but I am obviously emotionally charged about things that make code harder to read, and just grumpy in general (although I prefer the title General Grumpy).

5 Likes

#6

I should clarify that I don’t necessarily think cascade as it exists today is the ideal solution to the issues I highlight, only that it is one option to enable method chaining regardless of method types. The problems with cascade that you highlight are totally valid, and I’d very much like to see a better alternative to enable chaining/cascading of arbitrary methods!


I appreciate that you care about this issue, and am personally happy to have feedback on my post. For the most part I don’t think you’re being too harsh, the only thing that came across as a bit mean is:

This comes off as combative, and I think you could have made your point without it. The Rust community has a reputation for being welcoming and friendly, let’s keep it that way :smile:

5 Likes

#7

Fair point. I’ll try to be friendlier next time.

2 Likes