JDK-8191530 : fluent postfix notation for statically scoped interface methods
  • Type: JEP
  • Component: specification
  • Sub-Component: language
  • Priority: P4
  • Status: Draft
  • Resolution: Unresolved
  • Submitted: 2017-11-18
  • Updated: 2021-02-06
Related Reports
Relates :  
Description
fluent postfix notation for statically scoped interface methods

Interface default methods are an important way to express
overridable algorithms which all interfaces can share.
Defining a well-chosen set of default methods can
allow an interface (like Stream) to provide a "little DSL",
where method calls are applied to an object one at a
time, left to right, with each method result being the
receiver of the next method call.  Such a style is called
"fluent".

Occasionally an interface method cannot be expressed
as a default method, not because the fluent syntax
would be wrong, but for reasons of program security
and integrity.  In short, some methods cannot be
implemented reliably enough by interfaces, and so must
be invoked in a mode which does not allow overriding.

Immutable copying calls are the primary example of
this.  Allowing a `copyAsUnmodifiable` method
(aka. a "freezing" method) to be defined as a normal
default method would allow a broken implementation
to return a modifiable result.  Such errors happen
often enough to worry about, sometimes accidentally
and sometimes as part of a security vulnerability.

The language should allow some sort of marker on
static methods in interfaces that allows the compiler
to accept a call to the static method as if it were
a non-static method, with the first argument to the
method appearing in receiver position.

For example:

```
interface List<T> {
  __Fluent static <T> List<T> frozen(List<T> self) {
    if (self instanceof ImmutableList)  return self;
    return new ImmutableList(self);
  }
}
// and then:
<T> void processSecurely(List<T> input) {
  List<T> safeInput = input.asFrozen();
  ...do something confident that safeInput won't change...
}
// that was sugar for:
<T> void processSecurely2(List<T> input) {
  List<T> safeInput = List.asFrozen(input);
  ...do something confident that safeInput won't change...
}
```

A _fluent static_ call `ls.asFrozen()` would really be a static
call, indistinguishable from `List.asFrozen(ls)`.  The fluent
syntax would only be allowed if the type of the left-hand
expression `ls` did not already have a non-static method
usable, with the same name and compatible parameters.

```
// example of conflict between virtual and static:
class BadList implements List<Object> {
  List<Object> asFrozen() { return new WorseList(this); }
}
void doSomething(BadList input) {
  input.asFrozen();  // broken or nefarious call, but only on narrow type
  ((List<Object>)input).asFrozen();  // calls good fluent static 
}
```

The effect would be be something like the resolution of an
ambiguous method call `foo()` in the presence of both a
method `foo()` on `this` and also an import-static of `foo()`.
The virtual one takes precedence over the static.

Allowing a "final default" method in an interface is _not_ the
right approach, since it puts a heavy and uncontrollable constraint
on classes that would implement the interface.

A small advantage of fluent statics over regular methods
is that generic type inference is better over static method
arguments than over the receiver of a virtual generic method.

Besides freezing, the terminal operation of a builder
expression might need to be a fluent static instead of
a virtual, if there is some invariant (like stability) that
the builder interface itself wishes to enforce, and doesn't
trust to subtypes of the builder interface.  It depends
of course, whether a bad implementation could "leak"
into a builder expression, and then break the terminal
call.  In most cases I suppose that's not an issue.