`this` in concise method bodies
brian.goetz at oracle.com
Sat Oct 13 13:22:12 UTC 2018
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
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
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.goetz at oracle.com> wrote:
>> - 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.
> 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.)
> 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
> 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
More information about the amber-spec-experts