Forward references in initializers
Dan Smith
daniel.smith at oracle.com
Wed Nov 13 05:41:49 UTC 2024
> On Nov 9, 2024, at 2:59 PM, John Rose <john.r.rose at oracle.com> wrote:
>
> Boiling it down: Classify init actions (field initializers
> or instance initializers) as pre-super or post-super; only
> strict field initializers are pre-super. Then, require that
> a post-super init action must never precede (in left to right
> order) a pre-super init action (i.e., a strict field init).
>
> That would remove one kind of paradoxical appearance of
> disorder. Is it worth it? Maybe.
Eh, I'm open to it, but it seems kind of counter-productive—sure, a class that follows this rule avoids the tiny risk of surprising sequencing behavior, possibly improving readability (if they happen to do some other odd things); but it does it by forcing all authors to think/care about the sequencing in order to satisfy the rules. I'd rather most programmers just not think about it, and in the rare case that a problem occurs, the compiler can guide them.
> On Nov 11, 2024, at 4:05 AM, Maurizio Cimadamore <maurizio.cimadamore at oracle.com> wrote:
>
> The main point here is that whether strict fields are initialized before/after a super call is a very low-level detail that we'd like most developers to happily ignore. But if the distinction surfaces up at the level of DA/DU and field assignment, this is no longer strictly true, and it is possible that some developers might be puzzled as a result, and have to dig much deeper than they'd comfortable with to find exactly why that is the case. Preserving the illusion that all fields are created equal seems kind of nice, even though it is still an illusion.
Ok, but see my next message. :-) In order to use DA/DU to enforce requirements about initialization before 'this()'/'super()' calls, we need DA/DU to model the actual state of the variables at that point, not a simplified version that pretends all initializers run at once.
Where that leaves me is: even though it's a little surprising, the DA/DU anomaly I raised is not the end of the world, and the cure seems worse than the disease. Left-to-right is a reasonable approximation of what happens most of the time, but DA/DU needs to be a little more nuanced.
Note that for this anomaly to come up, we need two very cornery feature usages: (1) an instance initializer block (or, perhaps, some feature that introduces mixed early/late field initializers); (2) an assignment to a field within another field's initializer. Combine these two little-used features, and you get an initially-surprising but logical result. <shrug> If you don't like it, don't write your code that way.
> On Nov 11, 2024, at 4:54 AM, Brian Goetz <brian.goetz at oracle.com> wrote:
>
> I will reiterate my point that I think changing these rules is something that is more amenable to a strict class, then to a subset of the fields being strict. I continue to think that we are missing an abstraction here.
The source of complexity that you'd like to eliminate is this: some classes can have both early and late initializer code. In value classes, that manifests as an (early) field initializer plus a (late) initializer block. In uses of '!', that could manifest as mixed '!' and non-'!' fields (depending on how we handle this situation). Given a strictly-initialized-by-default class feature, it could also come up if there were a "but treat this one as late" opt-out.
To get to the happier space you're envisioning, we'd need two things:
- If any field initializers run early, all field initializers run early. With an appropriate class-level opt-in, this doesn't seem so bad. Corner-case must-be-late code can always be put in a constructor. I'm not sure where it leaves '!' types though.
- If there's any early initialization, initializer blocks also run early. But what's the point of an initializer block that can't touch 'this'? Maybe nothing, and we outlaw them in value/strict/whatever classes. Or maybe we support field reads during early construction (as we've discussed elsewhere), and I bet that would address most of the use cases for an initializer block.
I'd like to explore this further in some corpus code, but I kind of think getting past the field read limitation would go a long way to making strict-by-default everywhere viable (with, somehow, a minimal opt in for compatibility, giving authors a chance to specially handle things like cycles).
> On Nov 11, 2024, at 6:02 AM, Maurizio Cimadamore <maurizio.cimadamore at oracle.com> wrote:
>
> class Test {
> strict int x = 1;
> int y = x; // ok
> int z = 2;
> strict w = z; // error
> }
>
> Field initialization order is already fairly complex as it is (I mean remembering where a field initializer is effectively expanded into by the compiler). Adding new rules in this area is going to increase complexity.
Under the "status quo" rules that I'm defending, the error message here is straightforward: you can't reference 'z' in an early construction context. No need to worry about DA/DU.
That's the magic that lets us treat early execution almost as an optimization: your code doesn't have any dependency on 'this', so what difference does execution timing make to you, relative to the state of 'this'? ("Almost an optimization", because there's always the tiny risk that the initializer code depends on some external state for which sequencing is important.)
I'll note, though, that if we allow field reads in an early construction context, this whole story changes and we'll definitely need to guarantee left-to-right execution.
More information about the valhalla-spec-experts
mailing list