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