Language feature to support Fluent Syntax for Static Method Calls
Holo The Sage Wolf
holo3146 at gmail.com
Tue Mar 28 14:55:00 UTC 2023
What you are describing is Universal Function Call Syntax, and not Fluent
Syntax.
Fluent syntax is the pattern of ending a function with "return this" or
"return stage" to allow, well, a fluent API.
Universal Function Call Syntax is having multiple different syntaxes to
call a function.
The particular cases you are describing are solvable using Extension
Methods (i.e. Poor's man UFCS), and you can find an implementation of this
in Lombok [1] (note that Lombok uses several hacks for this).
[1] https://projectlombok.org/features/experimental/ExtensionMethod
On Tue, Mar 28, 2023, 15:39 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)
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20230328/eafbe0cd/attachment.htm>
More information about the amber-dev
mailing list