Superclasses for inline classes
John Rose
john.r.rose at oracle.com
Fri Dec 20 21:11:59 UTC 2019
On Dec 20, 2019, at 7:59 AM, Brian Goetz <brian.goetz at oracle.com> wrote:
>
> Stepping back, the thing that frightened us away from this was the combination of (a) not wanting to have a modifier on abstract classes to indicate inline-friendly,
Perhaps that’s made better by inverting the sense of the “bit” (perhaps modifier), so that inline-hostile classes have the burden of marking themselves, and inheritance can help with that (but only if the bit is inverted).
But inline-friendly types still need to contrive declaratively-empty constructors, so it looks like there’s a lot of recompilation in our future.
> and (b) worrying that it was a lot of (brittle) work to structurally detect inline-friendly abstract classes. Dan has cut this knot by tying it to the presence of a declaratively-empty constructor, which changes the story a lot.
This is a specific case of making abstract classes more like interfaces. If you look carefully at interfaces, you can see that their flexibility derives, in large part, from the lack of constructors. (This in turn implies the lack of instance fields, but in a secondary way: You can’t control such fields fully without a constructor.) Having declaratively empty constructors in classes (abstract or not!) opens for them some of the same paths that interfaces enjoy.
This reasoning can go the other way, too, to make interfaces more like abstract classes. As I’ve pointed out before, interfaces could be given explicit declaratively-empty constructors, which in turn could be given less-public access than the interfaces themselves. This would provide the same level of subclass control for interfaces as for abstract classes, with an effect close to “sealed interfaces”, from a class-like primitive (access control on constructors). In particular an interface could be sealed to its nest mates by marking its declaratively-empty constructor as private (and so on).
I’m not pointing this out to say we should done sealing differently; I love the way sealing turned out. But it’s important to be aware of some underlying “class physics” at play here with both interfaces and abstracts. As a VM guy I tend to see the “physics" this way as logical design constraints that bubble up from the VM, instead of starting with a desired psychology and working down through the chemistry (if you get my drift).
Over time I see interfaces becoming more like abstract classes (notably with default methods), and abstract classes returning the favor by growing declaratively-empty constructors. This is not an accident. I’m convinced that as we continue to pay attention to the “physics”, we will be better informed in our treatment of other aspects of types, including instance fields and identity.
> (I remain unconvinced that instance fields in inline-friendly abstract classes could possibly be in balance, cost-benefit-wise, and super-unconvinced about inlines extending non-abstract classes.)
My concern here is to point out the logical possibility of such things, not to advocate for them now. Treating fields and non-abstract supers as corollaries, rather than than axioms, makes me more certain we have grasped the physical essentials of the problem. This is a valuable design heuristic: We know that if we can say “no” to features while still understanding how they could fit in the future, we have arrived at a more factored, more desirable design. This is why I’m talking now, hypothetically, about fields and concretes.
(Which is more important, physics or psychology? Neither and both, I suppose, but physics has this privilege: If you build on an inconsistent or gratuitously complex logical foundation, your user experience will never ever be as smooth as it could be.)
> As John says, there are three potential states for an abstract type: always-identity (true of traditional abstract classes), always-inline, and identity-agnostic. (A possibly way to capture these is by leaning on our new friends IdentityObject and InlineObject; an always-inline abstract type implements InlineObject, and always-identity abstract type implements IdentityObject, and an agnostic one implements neither.) It is also possible we might prune away the always-inline flavor of abstract types, leaving us with two: inline-friendly or identity-locked.
I think the always-inline flavor of abstract *classes* can be replaced, for
most use cases, with interfaces (with sealing + default methods - toString),
and later on with templates (as long as the polymorphism is parametric).
Many uses of an always-inline flavor of *interfaces* can be replaced by a
sealed interface, where all the permits are inlines.
Dan and Remi have pointed out some places where we might be confronted
with a demand for more, which means we should keep our eyes open.
I’m very happy to bid less, for now, and perhaps forever.
> From a JLS perspective, we could say an abstract type is inline-friendly iff:
> - its supertypes are all inline-friendly
> - it has a declaratively empty constructor
(Yes and yes!)
> - it has no synchronized methods
I think the natural way to phrase this is that the type itself does not
admit synchronization, either via a synchronized method or a
synchronized statement. Then it’s a type system property rather
than a structural property of methods.
> - it has no instance fields
> - it has no instance initializers
Yes, although the latter condition might be stated more concisely that
all initializers must be static. The language partially obscures, but fully
obeys, the “physical” fact that all non-static initializers contribute effects
into every constructor. If there is just one declaratively-empty constructor,
that forces a contradiction. This contradiction can be documented in terms
of the practical effects you list.
(Defining a non-final non-static field does not directly contradict a
declaratively-empty constructor, but it motivates forbidding such fields
until and unless there is some new mechanism or workaround for
encapsulating them properly. Templates provide a workaround, and
protected init for blank finals could provide another, if we decided it
was worth doing, at some point in the future.)
> Object, and all interfaces, would be inline-friendly (we can adjust the declaration of Object to meet this requirement); the compiler would structurally recognize abstract classes as inline-friendly and set the bits in the classfile.
I like this, as long as “structurally” includes some explicit signal either in the
source code or (at least) in superclass (from which a new default would be
silently inferred). IMO there’s it’s hard to see a case for “promoting”
apparently-empty constructors (like C(){}) into declaratively-empty ones.
They would become invisibly-non-empty via action at a distance in supers
and in field definitions.
What I’m suggesting is that a class C can have either or neither of these two declarations:
C() { … }
abstract C(); //__DeclarativelyEmpty
But not both. And if neither is present, the rule applies for supplying a default
constructor: If there is no constructor at all, a nullary default is supplied if possible.
And *that* rule is modified to make the default constructor be declaratively
empty, if and only if the nullary super-class constructor is already in fact
declaratively empty.
(Did I miss anything?)
> In migrating Integer and friends to be inline-friendly (or inline-locked) abstract classes, all we do is push the fields and instance method implementations down into the primitive classes, which is fine because these classes are final.
Yes, that’s probably OK. Good!
Maybe there’s a role for templates to play also.
> (Making Integer an abstract class vs an interface means that int inherits the stupid statics on Integer, like the terminally confusing getInteger(String). Maybe some further deprecation of static inheritance is warranted here.)
(Yep, see other message.)
> If we say "instance fields is a can of worms" (which I think we should),
(for now, maybe forever)
> we get a further simplification: we don't need to deal with the combination of both declaratively-empty constructors and traditional constructors;
This is an interesting point, and I think it’s true under the assumption
that we don’t open that can of worms, yet or maybe ever.
The converse is also true: If there were some utility to have the *same
class* have *both inline and non-inline instances*, then it would need
*separate constructors*. That would incur the cost of shepherding the
worms as they crawl out of the can. I don’t have a proposal here, though,
at least not one that passes the red-face test.
> either you have real constructors (identity-locked) or empty ones (inline-locked/friendly), and in the case of an inline-friendly being extended by an identity class, the super constructor call is seen as a no-op.
Yes!
>> So, (3) allow the constructor with no arguments to be declared “abstract”
>> with no body. Amend the JVM rules to allow this, and to check upward
>> to the super for the same condition. During static checking of source,
>> treat such a constructor as empty,*and* as forbidding non-static initializers.
>> I think that gets what we need in a much clearer manner.
>
> The remaining bikeshed this leaves us is how to mark the constructor (abstract seems a good strawman), and whether to pull on the {Inline,Identity}Object levers.
+1 for abstract; I think it’s at least as good an analogy with
abstract methods and classes as “final”. Note that “abstract”
means “somebody else must supply my concrete bits”, and
that’s *exactly* the case here, where the only guy that supply
the constructor of an inline class is the inline class itself.
It’s feeling like progress here. (Thanks, Dan!)
— John
More information about the valhalla-spec-experts
mailing list