Method Chaining (enhancing Java syntax)
Mark Vieira
portugee at gmail.com
Thu Jun 8 13:34:54 UTC 2023
You can solve this problem by generifying the Shape class.
public class Shape<T extends Shape<?>> {
public T scale(double ratio) {
return (T) this;
}
}
public class Rectangle extends Shape<Rectangle> {
public Rectangle roundCorners(double pixels) {
return this;
}
}
On Thu, Jun 8, 2023 at 6:12 AM <blesa at anneca.cz> wrote:
> 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) { // <=== returned 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 will be 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 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/discuss/attachments/20230608/41edf603/attachment-0001.htm>
More information about the discuss
mailing list