Language feature to support Fluent Syntax for Static Method Calls

Kristofer Karlsson krka at spotify.com
Tue Apr 4 09:28:36 UTC 2023


Following up on the part about the String.transform method that was
added with JEP 326 that Rémi mentioned, is this something worth
considering adding for other classes and interfaces?

Specially, I think these ones would be very useful to have:

    interface CompletionStage<T> {
      default <R> R transform(Function<CompletionStage<T>,R> f) {
        return f.apply(this);
      }
    }

    class CompletableFuture<T> {
      default <R> R transform(Function<CompletableFuture<T>,R> f) {
        return f.apply(this);
      }
    }

    class Optional<T> {
      default <R> R transform(Function<Optional<T>,R> f) {
        return f.apply(this);
      }
    }

I could not find any old discussions as to why this would be a useful
thing to have for strings instead of just doing a regular method call:

The JEP shows:

    String stripped = `
                          | The content of
                          | the string
                      `.transform(MyClass::stripMargin);

but it could also have been written as:

    String stripped = MyClass.stripMargin(
                         `
                          | The content of
                          | the string
                      `);

so I think it is already a very similar use case to chaining method
calls related to futures.

Best regards
Kristofer

On Wed, Mar 29, 2023 at 1:43 PM Kristofer Karlsson <krka at spotify.com> wrote:
>
> Thank you all for your well thought out replies!
>
> I've taken some time to think everything through and made some mental
> adjustments:
>
> With regards to streams, I definitely agree that .toSet() would not be a
> significant improvement over .collect(Collectors.toSet()). That was a
> bad example.
> Also for controlling parallel streams, since that is typically done at the start
> of setting up a stream, it will be a limited problem in practice.
> I can't really come up with any other reasonable examples for streams,
> so perhaps
> that train of thought should be dropped.
>
> For CompletableFuture I think there are definitely some missing methods that
> should be added. It is somewhat surprising given that the API is in some ways
> very big where many methods do almost exactly the same thing.
>
> Some of that has been fixed already (thinking primarily of
> exceptionallyCompose),
> but isn't that itself somewhat of a signal that APIs can be imperfect and may
> sometimes need to be fixed. I imagine this will not be the last time
> that happens.
>
> So, I think the problem is still a problem - but do not have good insights into
> the size of the problem. For me it is mostly an inconvenience that leads to
> more verbose code, but I've seen other people at my work do the entirely wrong
> thing because the right thing to do was not easily available to them in the API.
> The impact of such mistakes comes in the form of performance issues
> (blocking threads in an environment that relies on asynchronous work
> with futures).
>
> Thinking about the solutions to the problem, there are many options that do not
> require any changes. Let's look at some of them, using
> exceptionallyCompose as an example:
>
> If this method exists (i.e. we're in Java 9+) we can do:
>
>     return someRemoteCall() // step 1
>      .thenApply(x -> x + x) // step 2
>      .thenCompose(x -> otherRemoteCall(x)) // step 3
>      .exceptionallyCompose(e -> retryRemoteCall(e)) // step 4
>      .thenApply(x -> transformResponse(x)) // step 5
>
> In Java 8 we can implement it manually roughly like this:
>
>     public static CompletableFuture<T> exceptionallyCompose(
>       CompletableFuture<T> future, Function<Throwable,
> CompletionStage<T> func) {
>       return future
>         .thenApply(CompletableFuture::completedFuture)
>         .exceptionally(func)
>         .thenCompose(Function.identity());
>     }
>
> Then we have many different ways we could write the code:
>
>     var f1 = someRemoteCall() // step 1
>        .thenApply(x -> x + x) // step 2
>        .thenCompose(x -> otherRemoteCall(x)) // step 3
>     return exceptionallyCompose(f1, e -> retryRemoteCall(e)) // step 4
>        .thenApply(x -> transformResponse(x)) // step 5
>
> Pro: method ordering matches the source code ordering
> Con: we need to introduce a variable for this. If this was in a lambda,
> we would need to use blocks.
>
> Or we could write it like this:
>
>     return exceptionallyCompose( // step 4
>       someRemoteCall() // step 1
>        .thenApply(x -> x + x) // step 2
>        .thenCompose(x -> otherRemoteCall(x)), // step 3
>       e -> retryRemoteCall(e))
>        .thenApply(x -> transformResponse(x)) // step 5
>
> The only problem with this is that the method ordering is less intuitive in the
> source code. Also, the parameter for exceptionallyCompose is 4 lines
> away from the
> method itself.
>
> Another alternative is to create a wrapper interface and use it like this:
>
>     return wrapAsFuture2(someRemoteCall()) // step 1
>        .thenApply(x -> x + x) // step 2
>        .thenCompose(x -> otherRemoteCall(x)) // step 3
>        .exceptionallyCompose(e -> retryRemoteCall(e)) // step 4
>        .thenApply(x -> transformResponse(x)) // step 5
>        .toCompletableFuture()
>
> Here the method order is preserved, but we need to create a full wrapper of
> the interface which is an extra bit of boilerplate code.
>
> I did not know about the (new) transform method in String - but that's also a
> useful pattern! If that was added to the most important classes and interfaces
> in the JDK, that would solve a lot of the concrete issues I think.
>
> With that, the code would look like:
>
>     return someRemoteCall() // step 1
>      .thenApply(x -> x + x) // step 2
>      .thenCompose(x -> otherRemoteCall(x)) // step 3
>      .transform(f -> exceptionallyCompose(f, e -> retryRemoteCall(e))) // step 4
>      .thenApply(x -> transformResponse(x)) // step 5
>
> which is not too bad.
>
> If we are talking about language features, I think two other approaches would be
> some postfix expression extension operator (previously referred to as thrush).
> With something like that, the code would look like:
>
>     return someRemoteCall() // step 1
>      .thenApply(x -> x + x) // step 2
>      .thenCompose(x -> otherRemoteCall(x)) // step 3
>      OP f -> exceptionallyCompose(f, e -> retryRemoteCall(e)) // step 4
>      .thenApply(x -> transformResponse(x)) // step 5
>
> Finally, the approach with alternative syntax for static method calls.
> If we are concerned with hiding the static method call and it looking
> too much like a regular call,
> we could do something similar to how method references work (::) - we
> would just need some
> simple way to make it look distinctly different.
>
> Example using double periods to denote imported static method call:
>
>     return someRemoteCall() // step 1
>      .thenApply(x -> x + x) // step 2
>      .thenCompose(x -> otherRemoteCall(x)) // step 3
>      ..exceptionallyCompose(e -> retryRemoteCall(e)) // step 4
>      .thenApply(x -> transformResponse(x)) // step 5
>
> Also, as is pointed out in this email thread, I agree that the fact
> that many other languages
> have tried to address this problem with some language feature, could
> be an indicator
> that it is in fact a problem worth solving.
>
> I have concluded that my original idea will not end up being
> implemented, but I hope
> that there is something simple that can be done to reduce the problem.
>
> Best regards
> Kristofer


More information about the amber-dev mailing list