Two remarks. First, I'm not sure what you mean by "static lambda". Of course it is already possible for a lambda to be declared as a static member of some class (static Runnable r = () -> {};), so you must mean something else, but I don't know what that is. Do you imagine a special declaration mode for a lambda that asks the compiler to ensure that it captures nothing?

Not that I am saying Java needs these, but ...

Lambdas can capture effectively final locals; those that happen to not do so are translated differently (the instance is cached at the capture side, so have better performance characteristics.)  Thing is an example of the compiler _inferring_ that the lambda could have been "static" (captures nothing from its context).  But the flip side is where the author would _declare_ that the lambda _cannot_ capture anything. This guarantees the better translation, but also gets better type chekcing -- if the author mistakenly captures something, they get a compiler error, rather than slower performance.  So this is what I mean by a "static lambda" -- a lambda that is declare to _not be allowed_ to capture anything.

It gets worse with anon classes, because we don't even do the inference and the better translation there when we can, and not capture the enclosing instance when it doesn't get used, causing more footprint and potentially unintended retention.

Second, how do code-containing constructs which are not methods or classes fit into this? I have field initializers in mind, but maybe static/instance initializers are relevant too. Does it make sense to nest a method inside of an instance initializer?

For purposes of the capturing rules, field initializers for { static, instance } fields should be treated as nested { static, instance } methods, and similar for { static, instance } initializers.

The question of whether to _allow_ local methods in these things is a separate story; there is a pro (consistency) and a con (ugh, really?) argument to be made there, but if we did, the capture rules would follow cleanly.

(This rabbit hole goes deeper; in theory there could be local methods wherever a statement goes, such as:

    int x = switch (z) {
        case 0 -> {
            int f(z) { ... }
            yield f(z);
        }
    }

... and one of these fellows could go in a static initializer.

Again, we get to decide how deep down the rabbit hole we want to go.)


On Tue, Jan 7, 2020 at 12:31 PM Brian Goetz <[email protected] <mailto:[email protected]>> wrote:

    Everything about nesting in Java is a mess.  The terminology is a
    mess (top level classes, nested classes, inner classes, local
    classes, anonymous classes); the set of restrictions on what can
    nest in what is ad-hoc (can have local classes but not local
    interfaces; inner classes cannot have static members, including
    static nested classes), and the set of rules about what must be,
    can be, or cannot be static is also ad-hoc (nested classes can be
    static or not, nested interfaces are implicitly static, but local
    and anonymous classes may not be static, even though it might make
    sense.)  On top of that, we can nest classes in methods
    (sometimes) and methods in classes but not methods in methods
    (local methods).

    Not only does this make for a lot of accidental complexity in
    specification, implementation, and user's brains, but it means
    every feature interact with this complexity.  Nested records are
    implicitly static, but this meant that in 14 we can't have nested
    records in non-static classes, because, non-static classes can't
    have static members.  (Yes, this could be fixed; hold your "why
    don't you just" suggestions.)  And we borked up the implementation
    of local records the first time around, where they accidentally
    capture effectively final locals, which they shouldn't -- because
    we'd never really charted the "static local class" territory, and
    got it wrong the first time.  (Yes, this can be fixed too, and
    will be before 14 goes out.)

    So, I'd like to propose a simpler, general story of nesting (which
    is consistent with the ad-hoc rubbish we have) which we can get to
    in stages.  The purpose of this mail is to discuss the model; in
    what increments we get there is a separate story.

    Goals:
     - Anything (class, interface, record, enum, method) can be nested
    in anything;
     - Some things are always static (enums, records, interfaces) when
    nested; the rest can be made static when desired;
     - The rule about "no static members in nonstatic nested classes"
    has to go;
     - Rules about whether members / locals from enclosing contexts
    can be specified in a single place, using local reasoning.

    The core of this is coming to an understanding of what "static"
    means.  When construct X nests in Y (whether X and Y are classes,
    methods, interfaces, etc), for "X" to be "static" means that
    nesting is being used purely for purposes of namespacing, and not
    for purposes of having access to names (locals or nonstatic class
    members) from enclosing constructs.

    Unfortunately all the terms we might use for whether or not a
    symbol in an outer construct can be used in a nested construct --
    such as "accessible" -- are overloaded with other meanings.  For
    purposes of this discussion, let's call this "capturable" (this is
    also overloaded, but less so.)  Each construct (class type or
    method) has two sets of names from outer constructs that are
    capturable -- a _statically capturable_ set SC(X), and a
    _non-statically capturable_ set NC(X).  We can define
    capturability using local reasoning:

    Base cases:
     - Names of static members in X are in SC(X);
     - Names of instance members of X (if X is a class) or effectively
    final locals of X (if X is a method) are in NC(X);

    Induction cases, where X is nested directly in Y:
     - SC(Y) is in SC(X)
     - If _X is not static_, then NC(Y) is in NC(X)

    We then say that X can capture names in SC(X) and NC(X); all we
    need to compute capturability is the capture sets of X's
    immediately enclosing construct, and whether X is static or not in
    that construct (modulo shadowing etc.)

    For the math-challenged, what this means is:
     - A nested construct can access static members of all the
    enclosing constructs;
     - A nested non-static construct can access instance members and
    effectively final locals of all enclosing constructs, up until we
    hit a static construct, and then capturing stops.  (So if Z is
    nested in Y is nested in static X, Z can access instance members /
    eff final locals of Y and X but not anything non-static from
    outside of X.)

    Note that this is consistent with what currently happens when X is
    a method as well as a class type; static methods in a class
    "capture" the static members of the enclosing class, and instance
    methods also capture the instance members of the enclosing class
    -- and also consistent with capturing in lambdas and anonymous
    classes, if we assume that these are always non-static constructs.

    We then say enums, records, and interfaces are _always_ static
    when nested, whether declared so or not, we eliminate the
    restriction about static members in non-static nested classes (now
    that we have a clear semantics for them), and allow local classes
    to be declared as static.  (Eventually, we also relax the
    restrictions about methods in methods, static or not.)

    (Additionally, the model supports the notion of "static lambda"
    and "static anonymous class" with obvious semantics (can't capture
    anything); we can decide later whether adding this flexibility is
    worth the additional surface syntax.)

    This is a strict superset of the status quo, and yields a more
    flexible and regular language -- and hopefully a simpler spec
    (since so many of these cases are specified as ad-hoc corner cases.)



Reply via email to