JEP draft: fluent postfix notation for statically scoped interface methods

OwnerJohn Rose
Componentspecification / language
Created2017/11/18 06:34
Updated2021/02/06 00:25

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(); something confident that safeInput won't change...
// that was sugar for:
<T> void processSecurely2(List<T> input) {
  List<T> safeInput = List.asFrozen(input); 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.