Language feature to support Fluent Syntax for Static Method Calls

Remi Forax forax at univ-mlv.fr
Tue Mar 28 20:47:15 UTC 2023


> From: "Tagir Valeev" <amaembo at gmail.com>
> To: "Brian Goetz" <brian.goetz at oracle.com>
> Cc: "Archie Cobbs" <archie.cobbs at gmail.com>, "Ron Pressler"
> <ron.pressler at oracle.com>, "Kristofer Karlsson" <krka at spotify.com>, "amber-dev"
> <amber-dev at openjdk.org>
> Sent: Tuesday, March 28, 2023 10:15:25 PM
> Subject: Re: Language feature to support Fluent Syntax for Static Method Calls

> By the way, my experience with extension methods in Kotlin is not very exciting
> (hopefully, my colleagues won't hate me on this). Probably it's the tooling
> problem, but it appears that it's too easy to call the extension method. Like
> imagine that you have an object myObj of type MyType and want to convert to the
> OtherType. You type myObj. and check the completion options and happily find
> that myObj.toOtherType() is suggested, which looks just like something you
> need. If you are not attentive enough you won't realize that toOtherType() is
> an extension method that was created in a completely unrelated module in a very
> specific context to solve a very specific problem, and this method has poor
> contract and is not applicable generally. Probably, it was declared as public
> by accident (partially because Kotlin is public-by-default) and was never
> intended to be used from outside. With a static method, you normally see which
> class the method belongs to, so such a problem doesn't happen. However, with
> extension methods, you just have a new entry in the imports list, which is far
> away from the use-site, so it's hard to notice what you are actually depending
> on.
> I made this mistake several times until I disciplined myself to check every time
> where the method comes from. And I saw such a thing done by other developers as
> well. Just recently I was working on IntelliJ IDEA project and refactored the
> JavaDoc inspection UI. I decided to remove a utility class (written in Kotlin
> and unfortunately public) that was created nearby solely to support the UI of a
> single inspection. However, our internal plugin compatibility tool yelled at me
> that there's a third-party plugin in our plugin repository that uses an
> extension method declared inside of that file. Of course, the plugin has
> nothing in common with JavaDoc inspection. I suspect that the plugin author
> just completed something without even checking where it comes from. Now, I need
> to keep this class and have a deprecation cycle in order not to break the
> plugin [1].
> So to summarize, ease of use of extension methods may suddenly become an
> unpleasant maintenance burden.

In the defense of Kotlin (or Groovy), extension methods are the only way to add a Kotlin feel to existing Java classes (think java.lang.String). 
If you do not have extension methods, the other solution is to rewrite all the base classes in your language, Ceylon choose that way, but it makes the interoperability with Java libraries unnecessary difficult. 

Extension methods are a practical solution for a language on top of Java to improve JDK classes while keeping a good interoperability with Java libraries. 
As a user of the language, I find extension methods less attractive because it makes the code harder to read. 

For the Stream API, we discussed about adding a method transform() working the same way as String.transfom() works [2], allowing a static method to be used in a chaining way, by example 

collection.stream() 
.transform(BetterStream::of) 
.parallel(executor) 
.toSet() 

> With best regards,
> Tagir Valeev.

regards, 
Rémi 

[2] https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/String.html#transform(java.util.function.Function) 

> [1] [
> https://github.com/JetBrains/intellij-community/blob/aa39823b7d3ed082888a749fe3051688be49d2fa/java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocUIUtil.kt
> |
> https://github.com/JetBrains/intellij-community/blob/aa39823b7d3ed082888a749fe3051688be49d2fa/java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocUIUtil.kt
> ]

> On Tue, Mar 28, 2023 at 9:58 PM Tagir Valeev < [ mailto:amaembo at gmail.com |
> amaembo at gmail.com ] > wrote:

>>> The second example -- changing how parallel execution works -- requires
>>> reinventing almost all of the implementation of Streams (which, if you've never
>>> looked at it, is a lot more complicated than you might think.) In which case
>> > the surface expression here is the least of your problems.

>> Sorry for moving the discussion away but I cannot stay aside when there's Stream
>> API on the table :-) Implementing .parallel(fjp) (fjp, not just any executor)
>> is not that hard as it seems. The only thing you need is to create a tiny
>> wrapper delegate over the original stream that remembers the supplied fjp, and
>> then submit every terminal operation to that fjp and join the result. I
>> implemented this in my StreamEx library [1], and this one is definitely not the
>> hardest Stream API extension that I implemented.

>> That said, as you need to wrap Stream API anyway, you can make it quite
>> comfortable without extension methods. You need to create a bunch of factories
>> repeating stream sources from JDK, like StreamEx.of(Collection) instead of
>> Collection.stream(). Not so huge work either. And then you can add .toSet() :-)

>> With best regards,
>> Tagir Valeev.

>> [1] [
>> https://www.javadoc.io/static/one.util/streamex/0.8.1/one.util.streamex/one/util/streamex/AbstractStreamEx.html#parallel(java.util.concurrent.ForkJoinPool)
>> |
>> https://www.javadoc.io/static/one.util/streamex/0.8.1/one.util.streamex/one/util/streamex/AbstractStreamEx.html#parallel(java.util.concurrent.ForkJoinPool)
>> ]

>> On Tue, Mar 28, 2023 at 9:11 PM Brian Goetz < [ mailto:brian.goetz at oracle.com |
>> brian.goetz at oracle.com ] > wrote:

>>> Not to pick on your example, but I'm going to pick on your example....

>>> You give as examples two methods you'd like to add to Stream: toSet and
>>> parallel(Executor) -- and it is notable that these examples are fairly commonly
>>> cited when this topic comes up. Note that the first is entirely a cosmetic
>>> thing; we already have collect(Collectors::toSet), so all this does is save a
>>> few characters -- its just code golf.

>>> The second example -- changing how parallel execution works -- requires
>>> reinventing almost all of the implementation of Streams (which, if you've never
>>> looked at it, is a lot more complicated than you might think.) In which case
>>> the surface expression here is the least of your problems.

>>> Now, it's easy for someone to complain "why didn't they make streams extensible"
>>> (we actually spent a lot of time exploring how this might work), but the
>>> reality is, Streams does not actually let users plug in new operations except
>>> through defined extension points like collect(), regardless of how easy or hard
>>> the language would make that. And the tricks that would create the illusion of
>>> doing so, like extension methods, force you to give up a significant portion of
>>> the nonfunctional value of streams, because a static "extension" method can't
>>> fuse operations, can't access the parallel machinery used by the rest of
>>> streams, can't interact with short-circuiting easily, can't take advantage of
>>> in-place optimizations, etc. So making a "static" extension look like a
>>> built-in method with chaining actually obfuscates what is going on, depriving
>>> readers of cues about the runtime behavior.

>>> Returning to your question, the problem of "wrapping streams" is one of the
>>> streams framework having a significant amount of complexity under the hood,
>>> which makes "tapping into it" hard -- and that's the real problem. And -- and
>>> here's the kicker -- this complexity shows up in most APIs that are candidates
>>> for heavy use of chaining anyway.

>>> On 3/28/2023 2:51 PM, Archie Cobbs wrote:

>>>> On Tue, Mar 28, 2023 at 10:48 AM Ron Pressler < [ mailto:ron.pressler at oracle.com
>>>> | ron.pressler at oracle.com ] > wrote:

>>>>> 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.

>>>> Great point - which also makes me curious how we should define the underlying
>>>> problem here.

>>>> One problem is "prettier chaining" which as Brian pointed out makes for a
>>>> relatively weak case.

>>>> What about another problem, which is that in Java it's too hard to "wrap"
>>>> something with new functionality? I.e., this is the same problem extensions try
>>>> to solve.

>>>> Just to be clear, suppose I invent this (using Kristofer's example):

>>>> public interface BetterStream<T> extends Stream<T> {
>>>> BetterStream<T> parallel(Executor e)
>>>> Set<T> toSet()
>>>> @Override
>>>> BetterStream<T> filter(Predicate<? super T> pred) // etc.
>>>> }

>>>> It's not easy to wrap Streams I encounter to convert them into BetterStreams. I
>>>> agree with Brian that "API designers should control their API's" so I suppose
>>>> we're talking about a true "wrap", not a "monkey patch". You can do a "wrap"
>>>> today but it's tedious and brittle. Could the language make it easier somehow?

>>>> I'm sure this has been discussed before. Curious what's the current status of
>>>> that discussion.

>>>> -Archie

>>>> --
>>>> Archie L. Cobbs
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20230328/87ecb90a/attachment-0001.htm>


More information about the amber-dev mailing list