Method Chaining (enhancing Java syntax)

Brian Goetz brian.goetz at oracle.com
Sun Jun 25 16:41:38 UTC 2023


As the replies indicate, this is a request that comes up periodically, 
and it is a topic that has been studied reasonably thoroughly.  Before 
commenting on the specifics of the various ways to get there (e.g., self 
types, special implied this-return treatment for void methods, etc), 
let's talk about what the goal is, and whether this is actually helpful.

Method chaining has pros and cons, but most developers focus only on the 
pros, and specifically, the part where they have to type fewer 
characters.  As one example, one of the downsides of chaining is that 
the type of the intermediate operations are obscured, such as in:

     stream.map(...)
           .sorted()
           .findFirst()
           .filter(p)     <--- here
           ...

Unless you are paying careful attention, you may miss the fact that the 
.filter() call here a call to Optional::filter, not Stream::filter.  
This may or may not be an issue, but the more chaining you have, the 
more likely that "calling a different method than you thought you were" 
errors can creep in.

Some APIs are explicitly designed for chaining, such as the 
java.util.stream library (Many such APIs are conceptually builders, even 
if they are not explicitly cast as such; streams is such an example.)  
The example above is usually mostly harmless, but when we go into CHAIN 
ALL THE THINGS mode, especially when we try to apply chaining to 
libraries that were not designed for chaining, we are more likely to 
skate out onto the edge.  All of this is not to say there is not a place 
for chaining, but that chaining is not the unalloyed good that many 
developers imagine it to be.

There are typically several sorts of proposals we see for MOAR CHAINING, 
in order of increasing hackiness:

  - Introduce a self type.  This allows libraries to say what they mean 
in libraries like your Shape example.  This is a principled feature that 
enables richer libraries, but does little for existing libraries which 
are full of setters or other mutative methods, which are often the 
source of requests like this.  (Even if we had such a feature, these 
libraries often cannot change their APIs for reasons of binary 
compatibility.)

  - Introduce a form of `let` expression that evaluates to a 
distinguished binding:

     ArrayList<String> a = let yield x = new ArrayList<>() in {
         a.add(3);
         a.add(4);
     }

This combines a traditional functional let-expression (let <bindings> in 
<expression>) with nominating one of the bound expressions as the value 
of the let body.  It is most useful when you want to create an object, 
do a bunch of mutations on it, and then pass the result along.

  - Introduce a feature by which a void method can can say "treat 
invocations of me as if they evaluate to the receiver" (I think this is 
closest to what you are suggesting today), such as:

     public this-return foo() { ... }

Such a method would *compile* to a void method, but the language would 
treat an invocation of `x.foo()` as evaluating to `x` rather than void.  
This has the advantage that you could compatibly apply `this-return` to 
existing methods (such as setters) in a source- and binary-compatible way.

  - Go even farther, and apply the above treatment to *all* void methods.


These proposals are not intrinsically bad, but they are also not that 
compelling, because they add complexity for limited incremental 
expressiveness.  (The self-types one is the most compelling, but it is 
also the most significant effort.)

One reason they are not that compelling (especially the latter three) is 
that they largely exist primarily as workarounds for the following idiom:

     x = new Foo();
     x.setA(...);
     x.setB(...);
     yield x;

so that all of this can be done as an expression.  (I appreciate the 
motivation here; expressions are better than statements, because they 
compose.)

This is an idiom that shows up frequently with mutable JavaBean-style 
APIs, but both the language and the ecosystem have been moving away from 
this style, and so having a specialized language feature for it is sort 
of "fighting the last war."  Attila K pointed out that newer APIs can 
provide (or you can write your own) methods that have this affect as well:

     static<T> with(T t, Consumer<T> c) { c.accept(t); return t; }
     ...
     Foo f = with(new Foo(),
                  f -> { f.setA(3); f.setB(4); });

(And, in case its not obvious, every language feature competes with 
every other one; doing this means not doing something else, and the 
"something else" alternatives always seem to be more compelling.)




On 6/9/2023 12:35 PM, Tomáš Bleša wrote:
> //* I sent the following to discuss@ mailing list yesterday. (wrong 
> list for the topic) I hope this will be more appropriate mailing list *//
>
> Hi all,
>
> this is a request for feedback on a topic that has been on my mind for 
> a few weeks. I have written a short document in JEP format and would 
> like to ask you to comment if you find the described proposal useful.
>
> Thanks,
> Tomas Blesa
> __________________________________
>
> Summary
> -------
> Enhance the Java language syntax to better support the method chaining 
> (named parameter idiom) programming pattern.
>
> Goals
> -----
> The primary goal is to remove unnecessary boilerplate code in class 
> methods designed for type-safe chained calls, especially when combined 
> with inheritance.
>
> Motivation
> ----------
> [Method chaining](https://en.wikipedia.org/wiki/Method_chaining) is a 
> widely used and popular programming pattern, particularly in creating 
> libraries (APIs) or configuration objects. Programmers can easily 
> create a method that returns `this` with a method signature that 
> specifies the returning type of the containing class.
>
> ```java
> class Shape {
>     public Shape scale(double ratio) {
>         // recalculate all points
>         return this;
>     }
> }
> ```
>
> The problem arises when we combine this pattern with inheritance. We 
> can lose type information when calling the method on a subclass. For 
> example, let's create two subclasses of the `Shape` superclass:
>
> ```java
> class Rectangle extends Shape {
>     public Rectangle roundCorners(double pixels) {
>         // ...
>         return this;
>     }
> }
>
> class Circle extends Shape {
> }
> ```
>
> Now, imagine the following piece of code using the mini-library above:
>
> ```java
> var myRect = new Rectangle().scale(1.2).roundCorners(10);
> ```
>
> The code won't compile because `scale()` returns the type `Shape`, 
> which doesn't have the `roundCorners` method. There is also a problem 
> even without the final `roundCorners()` call:
>
> ```java
> var myRect = new Rectangle().scale(1.2);
> ```
>
> The inferred type of `myRect` is `Shape` and not `Rectangle`, so the 
> following line will also be invalid:
>
> ```java
> myRect.roundCorners(10);
> ```
>
> Straightforward solutions to the problem could be:
>
> 1) Override the `scale()` method in all subclasses and change the 
> return type:
>
> ```java
> class Rectangle extends Shape {
>     // ...
>     @Override
>     public Rectangle scale(double ratio) {
> super.scale(ratio);
>         return this;
>     }
> }
> ```
>
> 2) Split object construction and method calls:
>
> ```java
> var myRect = new Rectangle();
> myRect.scale(1.2);
> myRect.roundCorners(10);
> ```
>
> 3) Partial solution - reorder chained calls (if possible):
>
> ```java
> var myRect = new Rectangle();
> myRect.roundCorners(10).scale(1.2); // roundCorners called first
> ```
>
> All of these solutions add unnecessary lines of code, and as the 
> library of shapes grows, keeping the desired return type will 
> introduce more and more boilerplate code.
>
> Description
> -----------
> The proposed solution to the problem described in the previous section 
> is to extend the Java syntax for the returned type in method signatures:
>
> ```java
> class Shape {
>     public this scale(double ratio) { // <=== returns this
>         // recalculate all points
>         return this;
>     }
> }
> ```
>
> Methods declared or defined as returning `this` can only return the 
> instance on which they are called. The following code will be 
> type-safe and perfectly valid:
>
> ```java
> var myRect =                   // inferred Rectangle type
>     new Rectangle()              // returns Rectangle instance
> .scale(1.2)                  // returns Rectangle instance
> .roundCorners(10);           // returns Rectangle instance
> ```
>
> The constructed type `Rectangle` is preserved throughout the entire 
> call chain.
>
> It is possible to override methods returning `this`, but the subclass' 
> implementation must also be declared with the `this` keyword instead 
> of a concrete returning type.
>
> It is even possible to remove the explicit return statement altogether:
>
> ```java
> class Shape {
>     public this scale(double ratio) {
>         // recalculate all points
>     }
> }
> ```
>
> Or simply remove the value `this` from the return statement:
>
> ```java
> class Shape {
>     public this scale(double ratio) {
>         // recalculate all points
>         if (condition) return;         // <== automatically returns this
>         // do something else
>     }
> }
> ```
>
> In fact, methods returning `this` can be compiled to the same bytecode 
> as methods returning `void`. This is because the instance reference 
> (and the returned value) is already known to the caller, eliminating 
> the need to pass that value back through the call stack. As a result, 
> both CPU cycles and memory are saved.
>
> In the Java world, it is common to create getters and setters 
> according to the Java Beans specification in the form of 
> `getProperty`/`setProperty` pairs or `isProperty`/`setProperty`. 
> Setters are defined as returning `void`. These setters can be more 
> useful if defined as returning `this`:
>
> ```java
> class Customer {
>     public this setFirstname() { ... }
>     public this setSurname() { ... }
>     public this setEmail() { ... }
> }
> ```
>
> This allows for more concise usage when constructing and configuring 
> an instance without adding more code:
>
> ```java
> customers.add(
>     new Customer()
> .setFirstname(resultSet.getString(1))
> .setSurname(resultSet.getString(2))
> .setEmail(resultSet.getString(3))
> );
> ```
>
> It is also possible to declare an interface with methods returning `this`:
>
> ```java
> interface Shape {
>     this scale(double ratio);
> }
> ```
>
> In this case, all implementing classes must define the method as 
> returning `this`.
>
> The proposed syntax is a bit less useful for enums or records, as 
> neither of them allows for inheritance. But enums and records can also 
> implement interfaces and for this reason and for overall consistency, 
> "return this" syntax should be allowed for enums and records.
>
> To accommodate the syntax with the Java Reflection API, it will 
> probably be required to create a special final placeholder class 
> `This` (with an uppercase "T"), similar to `java.lang.Void`.
>
> Alternatives
> ------------
> It is probably possible to help auto-generate overriding methods in 
> subclasses using annotation processing, but this option wasn't fully 
> explored. However, such an approach would add extra unnecessary code 
> to compiled subclasses and go against the primary goal of reducing 
> boilerplate.
>
> Risks and Assumptions
> ---------------------
> The proposed syntax is likely to break the compatibility of 
> library-dependent code whose author decides to switch to the "return 
> this" syntax between versions.
>
> Older code that looks like this:
>
> ```java
> class MyUglyShape extends Shape {
>     @Override
>     public MyUglyShape scale(double ratio) {
>         return this;
>     }
> }
> ```
>
> will have to be rewritten as:
>
> ```java
> class MyUglyShape extends Shape {
>     @Override
>     public this scale(double ratio) {    // signature change
>         // optional removal of the return this statement
>     }
> }
> ```
>
> or
>
> ```java
> class MyUglyShape extends Shape {
> // override removed
> }
> ```
>
> This problem can be mitigated with the help of smart IDEs 
> automatically suggesting such refactoring.
>
> Another possible risk is breaking old code that relies on the 
> Reflection API for scanning the returning types of methods.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/jdk-dev/attachments/20230625/b38d2a3f/attachment-0001.htm>


More information about the jdk-dev mailing list