Method Chaining (enhancing Java syntax)

Remi Forax forax at univ-mlv.fr
Sun Jun 25 17:02:40 UTC 2023


Another proposal is to use a use-site syntax instead of a declaration site syntax. 
By example, Dart uses the syntax ".." 
new Foo()..setA(12)..setB(42) 

And this also can be seen as a poor's man substitute for keyword based method calls 
var foo1 = new FooRecord(a: 12, b: 42); 
and the splat operator 
var foo2 = new FooRecord(a: 13, ...foo1); 

Rémi 

> From: "Brian Goetz" <brian.goetz at oracle.com>
> To: "Tomáš Bleša" <blesa at anneca.cz>, "jdk-dev" <jdk-dev at openjdk.org>
> Sent: Sunday, June 25, 2023 6:41:38 PM
> Subject: Re: Method Chaining (enhancing Java syntax)

> 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 |
>> 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/044fd86b/attachment-0001.htm>


More information about the jdk-dev mailing list