John makes some good arguments for the value of the :: form of CMBs;
that they raise the level of reuse from imperative to declarative using
a variety of wiring patterns. I'd like to make a case for an
under-appreciated aspect of the -> form as well.
It's natural to look at a feature like this and imagine how we might use
it to re-express existing code. In this case, some people are looking
at it and saying "but my existing code is OK, and this doesn't make it
that much better." A fair reaction, but let's remember this is only
half the story. The other half is, how might it _change_ the ways we
code (for better or worse.)
I'll make an analogy to local variable type inference. It's obvious how
existing code can take advantage of LVTI -- just s/explicit type/var/.
Fine. But, what's less obvious is how lowering the barrier to declaring
a local variable moves the equilibrium of how people factor expressions,
towards simpler expressions. LVTI reduces the "penalty" for unrolling
complex nested/chained expressions into a sequence of simpler
expressions, where each subexpression has a descriptive name:
var a = ...
var b = f(a)
var c = g(a,b)
(OK, a/b/c are not descriptive names, but you get the point.) We've all
felt the temptation to inline a subexpression into an already complex
expression, even though it would be more readable to give it a name,
because the overhead of declaring it felt like "too much". LVTI lowers
that activation energy, giving us more choices, and nudging us away from
the cram-it-all-in style.
(To be fair, people are aware of some of the ways in which a new feature
might change how people code -- but generally they see the bad ways much
more readily than the good. People were very quick to jump on how bad
programmers might misuse LVTI; they were much slower to realize how it
would nudge most programmers towards writing clearer code. (Nod
silently if you recognize this dynamic.))
The -> form of CMBs have a similar characteristic to LVTI; they lower
the overhead of factoring a subexpression into a method. As a result,
we should expect people to code with larger numbers of simple methods,
each with a descriptive name. Isn't this something we should be nudging
people towards? A method that evaluates exactly one expression is a
generally a pretty good method; with this form, we'll surely get more of
those:
int a() -> ...
int b() -> f(a())
int c() -> g(a(), b())
Or, to put it the other way, the status quo (to which we're all used)
discourages this normalized form (which functional programmers will
recognize instantly), by blurring the distinction between cleanly
factored methods like these, and messier methods that do more than one
thing.
While small, I think this is a nudge worth considering.
On 10/13/2018 12:47 AM, John Rose wrote:
On Oct 12, 2018, at 10:15 AM, Brian Goetz <brian.go...@oracle.com> wrote:
Summary:
- Both the capture-this and drop-this cases have important motivating use
cases
- Arbitrarily dropping one or the other would compromise the feature
- There are some possibly reasonable ways of doing overload resolution and
adaptation here, at some complexity.
+100
One reason this proposal is so very powerful is that it allows the
original 'this' passed to the CMB-defined method to serve either,
or both, or neither of two independent roles:
Use-1. Find a data-dependent object (a field 'f' of 'this', or 'this' itself)
to delegate the operation; this delegate will execute the target method
as the next 'this'. The method reference is of the form 'this::target'
or 'field::target' (where 'field' is treated as 'this.field').
Use-2. Pass 'this' as a passive (non-receiver) argument to the target
method, which may choose to use the original 'this' value in some way.
The method reference is of the form 'sf::target' or 'T::target', where 'sf'
may be a static field or perhaps another constant.
(Non-use-1. If 'this' does not locate a data-dependent object, such an
object may still be obtained from another source 'x', such as a static
variable in the class or another parameter of the method. The
method reference is of the form 'x::target'. The method reference
may also refer to a static method, as 'T::sm', in which case there
is no receiver needed, and no data dependency at all.)
(Non-use-2. If 'this' is not passed as a passive argument, then
only the explicit arguments of the original method are passed.)
Use-1 vs. non-use-1 is determined by the expression before
the '::' in the method reference. Does this expression make
use (explicit or implicit) of 'this', or does it only use statically
available names and parameter names?
Use-2 vs. non-use-2 is determined by the arity of the matching
method: Does it accept the use-2 passive argument value 'this',
in which case this value is injected as a new passive argument,
or is it dropped?
There are thus four shapes of target method invocations, with
respect to their use or non-use of 'this':
Static call (neither 1 or 2): The target method uses only the
explicit parameters. It may be as simple as a constant-returning
method, or a method that derives a value from one of the arguments.
Delegate or bridge call (1, not 2): The target method is called on a
"friend" of the the original object. The original object may call
a different method on itself; this is a bridging pattern.
Concept invocation (2, not 1): The class of 'this' (but not 'this'
itself) declares a handler method to execute on behalf of
the original 'this', which is passed as an argument.
Prototype invocation (both 1 and 2): The original receiver
object passes the request to a friend object, *and* passes
along its own identity. It is as if each object has the option
of carrying around its own customized Concept, rather than
all objects of a given class using a common Concept.
(The term "Concept" comes from C++. I'm not fond of it,
but I don't have a better term than "function". In Lisp or
Haskell everything is a "Concept". What a concept.)
Examples:
int computeLength(String s) = String::length; // Static for some LengthComputer
int getAsInt() = MY_RAND::nextInt; // Static for some IntSupplier
int size() = inner::size; // Delegate for some wrapping List
T get(int x) = inner::get; // Delegate for same
long longHash() = this::hashCode; // Bridge for some LongHashable
long longHash() = ThisClass::hashCode; // Same effect via different path
long longHash() = ::hashCode; // Same effect via different path
void reverse() = Collections::reverse; // Concept for some List
int compareTo(List that) = MY_LEX_COMPER::compare(); // Concept for some List
String toString() = MY_TO_STRINGER::stringOf; // Concept for some Object
void mouseClicked(MouseEvent e) = myEventParent::mouseClickedFor; // Prototype
(Similar comments might be made about patterns which delegate
to explicit method parameters, which in some sense are "just as
deserving of attention" as the implicit 'this' parameter. Delegating
to an explicit parameter amounts to an immediate callback.
However, the CMB proposal doesn't need to support such things
via method references, and the question of dropping a non-'this'
parameter would seem to be a vexed one.)
The Prototype pattern may seem far-fetched, but there are times
when it's useful. It has been used to to join and generalize both
regular class-based inheritance and delegation; in this use the
delegate is called the "parent" in Self and "protoype" in JavaScript.
But I don't need to plump for Prototypes in order to observe that
the two "axes" of method reuse, Delegation and Concepts, are
both really, really useful by themselves. If I had to choose one
this-using pattern, it would be Concepts, but I think it would be
hard to drop Delegation given the natural way it also fits into the
CMB proposal. Since CMBs give us all four patterns under one
powerful rubric, I say let's take all four and say thank you.
— John