Language feature to support Fluent Syntax for Static Method Calls

Ron Pressler ron.pressler at oracle.com
Tue Mar 28 15:44:53 UTC 2023


The best and surest way to impact the evolution of Java is not by offering solutions, but by reporting problems. Java is a conservative language, i.e. we are more reluctant to add language features than other languages, and the bar for a language feature is relatively high: it should either solve a big problem or multiple small problems (the more magic the feature adds, the higher the bar).

The problem you reported is that composing operations using static methods may be “clunky”, but you also provided two specific examples (good!). Unsurprisingly, both concern “functional pipelines” made up of higher-order operators. It’s hard to judge how big of a problem this clunkiness imposes in general, but before even thinking about whether or not this justifies a change to the language, we can study these examples more carefully.

Composing asynchronous operations with CompletableFuture may indeed be clunky, but the syntactic aesthetics is the least of the problem. Context is lost, language control flow and exceptions don’t work as they normally do, debugging is very difficult and profiling is almost impossible. These problems are so big that they justified an bigger change, virtual threads, that allow you not only to compose operations using the language’s built-in composition, but also give back context, and work well with tooling. If you want nicer composition, consider using a virtual thread, and you’ll gain other benefits, too.

As for streams, it is, indeed, aesthetically unpleasing at times to add user-provided operators to the pipeline. But that could be a result of a deeper inflexibility of streams, and making streams more flexible would yield higher dividends in this case than a new language feature.

I would also suggest that:

    var s = parallel(list.stream(), myExecutor)
        .map(x -> x + 1)
        .limit(10);
    var mySet = toSet(s);

is quite readable. The question of, “can this code be written in a way that a reader will easily understand what it does?”, is more important to us than the question, “can this code be written in a way that the author finds pretty?” however tempting it is to add a feature that makes code looks pretty (and sometimes prettier may actually mean *harder* to understand).

As usual, the main challenge is understanding what exactly is the problem here — is this a specific issue with CF and Stream or something more general — and if there is a general problem, what exactly is it, and does it justify a change to the language. Only after we answer that can we consider adding a language feature.

— Ron

> On 28 Mar 2023, at 13:37, Kristofer Karlsson <krka at spotify.com> 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 to https://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)



More information about the amber-dev mailing list