Language feature to support Fluent Syntax for Static Method Calls

Brian Goetz brian.goetz at oracle.com
Tue Mar 28 16:57:12 UTC 2023


To Ron's excellent answer, I'll add a few more points about things 
already considered-and-rejected.

  - As the linked SO post indicates, we explicitly decided against 
extension methods on the basis that they are a _bad idea_.  This differs 
from many features that might be reasonable ideas, but just don't rise 
to the level of worth changing the language for.  Extension methods are 
a feature that explicitly subverts one of the important values of the 
platform -- among their many other weaknesses, such as poor 
discoverability and non-extensibility.

  - During the development of the streams API, we did consider the 
question of extensibility, and whether some additional support for 
chaining static operations was valuable, similar to the "thrush" 
operator in Clojure 
(http://blog.fogus.me/2010/09/28/thrush-in-clojure-redux/). This is a 
more explicit syntax for "call this thing with the LHS as the first 
argument", something like `stream |> map(...) |> filter(...)`.  As Ron 
pointed out, while this holds out the promise of extensibility, it would 
not actually offer a lot of incremental power for streams, because the 
ability to "extend" streams with static methods is in reality quite 
limited.  So at the time, this seemed mostly a "shiny feature idea in 
search of a real problem."

There are a number of feature ideas centering on method chaining that 
keep coming around every few years.  My opinion is that they are mostly 
"weak" features, because method chaining itself is pretty weak.  In the 
cases where it works out, it is pretty nice, but the set of cases where 
it is actually a useful structuring convention for APIs is quite 
limited, and it is often badly overused.  (A prime example is "async" 
libraries; these illustrate the pitfalls of designing APIs around an 
idiom that only permits linear composition.  The simplicity of 
"filter-map-reduce" quickly gives way to becoming a programming 
straitjacket.)

Further, the call for more features around chaining often centers around 
superficial aesthetics rather than expressiveness, because chaining 
itself runs out of expressive ability quickly enough.  So a feature 
whose primary goal is "moar chaining" is suspect out of the gate.




On 3/28/2023 8:37 AM, Kristofer Karlsson wrote:
> Hi, I am new to this list so apologies if I'm missing any guidelines to follow.
>
> This feature idea includes a specific solution, but I think the
> feature (some way to support a more fluent code flow with methods that
> are not part of the object) is more important than the solution
> (Allowing a new syntax for static method calls) so even if the
> solution ends up being rejected, I hope the feature itself does not
> have to be.
>
> I have tried searching for existing JEPs that roughly matched this
> feature idea but could not find anything. Before trying to create an
> actual JEP, I thought it would be useful to know if this is a
> reasonable idea at all. The closest thing I've been able to find is
> this stackoverflow post[1] where extension methods are discussed.
> However, I'm thinking about this problem in terms of enabling a more
> fluent code style at the use-site without changing any semantics, and
> not in terms of designing and controlling APIs (for which the existing
> interface abstractions already work very well).
>
> [1]:https://stackoverflow.com/questions/29466427/what-was-the-design-consideration-of-not-allowing-use-site-injection-of-extensio/29494337#29494337
>
> Best regards
> Kristofer
>
> ---
>
> Title: Fluent Syntax for Static Method Calls
> Author: Kristofer Karlsson
> Created: 2023/03/27
> Type: Feature
> State: Draft
> Exposure: Open
> Component: specification / language
> Scope: SE
> Discussion: amber dash dev at openjdk dot java dot net
> Template: 1.0
>
> ## Summary
>
> Enhance the language by allowing static method calls to be used in a fluent way.
>
> ## Goals
>
> Enhance the language and compiler to support invoking static methods
> in a fluent way. This would be a purely ergonomic change to help
> developers to write code that is easier to read, write and maintain
> without breaking any existing code and without requiring any changes
> to the bytecode or the runtime.
>
> Fluent in this context refers tohttps://en.wikipedia.org/wiki/Fluent_interface
>
> ## Motivation
>
> Given the JDK additions of streams and classes such as CompletableFuture, and
> already existing patterns of using builders and operating on immutable types,
> writing code in a fluent style has become common, and is considered best
> practice in many cases.
>
> For streams you would do something like:
>    list
>      .stream()
>      .map(x -> x + 1)
>      .limit(10)
>      .collect(Collectors.toSet())
>
> and with CompletableFuture you would do something like:
>    CompletableFuture.completedFuture("hello")
>      .thenApply(s -> s + " world")
>      .exceptionally(e -> "An error occurred")
>      .thenRun(this::someCallback);
>
> These classes and interfaces are out of our control - they are part of the JDK -
> so we can not extend them to add other potentially useful methods such
> as Stream.parallel(executor), Stream.toSet(),
> CompletableFuture.handleCompose(func),
> CompletableFuture.orTimeout(time, unit, executor).
>
> We can create static methods that operate on such classes and return
> new instances,
> but chaining such calls together becomes clunky. Here's a (somewhat contrived)
> example of what it would look like today vs what it would look like with
> the proposal in place:
>    toSet(parallel(list.stream(), myExecutor)
>      .map(x -> x + 1)
>      .limit(10))
>
> compared to:
>    import static MyStream.parallel;
>    import static MyStream.toSet;
>
>    list.stream()
>      .parallel(myExecutor)
>      .map(x -> x + 1)
>      .limit(10)
>      .toSet()
>
>
> And for CompletableFuture:
>    orTimeout(
>      handleCompose(
>        flatten(
>          CompletableFuture.completedFuture(null)
>           .exceptionally(e -> new CompletableFuture<>())),
>        (v, e) -> ...),
>      10, TimeUnit.SECONDS, myExecutor)
>
> compared to:
>    import static MyFutures.flatten;
>    import static MyFutures.handleCompose;
>    import static MyFutures.orTimeout;
>
>    CompletableFuture.completedFuture(null)
>      .exceptionally(e -> new CompletableFuture<>())
>      .flatten()
>      .handleCompose((v, e) -> ...)
>      .orTimeout(10, TimeUnit.SECONDS, myExecutor)
>
> Note: CompletableFuture.exceptionallyCompose is an example of a method that was
> initially missing but later added due to being convenient, even though it could
> have been implemented as a simple chain of calls:
> future.thenApply(CompletableFuture::new)
>    .exceptionally(e -> func(e))
>    .thenCompose(Function.identity())
>
> # Description
>
> Currently static method calls look like:
> ClassName.methodName(arg1, arg2, ...);
>
> Combined with a static static import statement they can also look like this:
> import static ClassName.methodName;
> methodName(arg1, arg2, ...);
>
> The compiler could be modified to also recognize this form:
> import static ClassName.methodName;
> arg1.methodName(arg2, ...);
>
> For simplicity, this would only be allowed for statically imported methods.
> This means that you could invoke Arrays.fill() as:
> import static java.util.Arrays.fill;
> int[] array = ...;
> array.fill(0);
>
>
> ## Out of scope
> Technically, this feature could also be allowed for primitives. This
> would mean that
> you could invoke Math.abs(-1L) as (-1L).abs() and Math.exp(2.0, 3.0)
> as (2.0).exp(3.0).
>
> However, this may introduce extra complexity for the compiler to
> handle, so for the purpose
> of simplifying the JEP, this is out of scope.
>
> ## Compatibility
>
> All existing valid code will continue to be valid and behave exactly as before.
>
> ## Risks
>
> If new code introduces usage of this new method invocation, there could be a
> subtle change to the method resolution if the argument type introduces a
> method that matches the signature. Recompiling the class after the argument type
> dependency has been updated would lead to a different method resolution.
>
> However, this is already true for a similar case:
>
> public class A {
>    public static void main(String[] args) {
>      (new B()).theMethod();
>    }
> }
>
> public class B {
>    static void theMethod() { }
> }
>
> will show that invoking B.theMethod() resolves to invokestatic:
> 8: invokestatic  #4                  // Method B.theMethod:()V
>
> If B is then changed to:
> public class B {
>    void theMethod() { }
> }
>
> and we recompile A, we get:
> 7: invokevirtual #4                  // Method B.theMethod:()V
>
> If we don't recompile A, we get this error instead:
>
> Exception in thread "main" java.lang.IncompatibleClassChangeError:
> Expected static method 'void B.theMethod()'
> at A.main(A.java:3)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20230328/9ffe8bd9/attachment.htm>


More information about the amber-dev mailing list