While working with initializers, I was surprised to find that if a superclass defined a single
initializer, I had to define an explicit initializer for its subclass even though it seemed
obvious what that initializer should be... I'd expected that the compiler would provide it
in cases like this.
There's arguably a question as to what an "obvious parent initializer" should be. In this
case, I think I'm arguing "if there's just a single initializer, it should essentially pass
through." Others may want to come up with more involved definitions.
Associated Future Test(s):
test/classes/initializers/inheritance/defaultForInheritedInit.chpl #8141
chpl --version: chpl Version 1.17.0 pre-release (14176f2)(I know you know, but for posterity) note that this is brought up as a possible extension in the initializers chip (see the end of this section)
Michael pointed out that Swift's designated initializers might influence our approach to this kind of program:
https://docs.swift.org/swift-book/LanguageGuide/Initialization.html#ID222
I think the use of parent fields in the child's fields may complicate matters:
class Parent {
var x : int;
var y : real;
}
class Child : Parent {
var z = x * y;
}
Here the default initializers are fairly straightforward:
proc Parent.init(x : int = <default int>, y : real = <default real>);
proc Child.init(x : int = <default int>, y : real = <default real>, z = x * y);
If we add a user-defined initializer to the parent, then we cannot use the formals for the parent in the default value of the z formal:
proc Parent.init(val : int) {
this.x = val;
this.y = val:real;
}
proc Child.init(val : int, z = ???);
var c = new Child(5); // wants default for 'z'
Do we want to still handle the simple case and issue an error for the case I just described? Or do we want to keep it simple and just not support this at all?
That is tricky. I'm not sure there's an obvious initializer we could generate that would be right in all scenarios, and after a certain point the complexity of adjusting for different situations would just tangle up the compiler for cases the user could and possible should handle themselves.
This example makes me think we should keep this simple and not support this at all, at least for the time being. I.e., the presence of an explicit user-defined initializer on the parent disables the default initializer for the child. Based on the .bad for my future in this issue, I'm guessing that this is not what we have today? (i.e., it looks like there's a 0-argument initializer generated for D?) Which makes me wonder whether we have (important) tests that would start breaking if we made that change?
_If_ we wanted to come back to this in the future (and TBH, I'm not sure we should given the "if the parent has a single initializer" caveat that follows...), my main thoughts are:
1) For a parent class with a single initializer, we could potentially make the default init for z be proc init(<all args from parent's single initializer>, z = <default real>) ... and then add the long-desired support for querying "Did the user supply this argument or rely on the default?" so that within the body of a routine, where we could set z to this.x + this.y if they relied on the default and the value passed in if they did not. In pseudo-code for your example, I think this would look like:
proc init(val: int, z = <default real>) {
super.init(val);
if z.defaultOverridden then
this.z = z;
else
this.z = this.x + this.y;
}
In some ways this actually seems truer to Child's actual definition in that my interpretation of zs default value is that it's based on the x and y _fields_, not the arguments that set them up. As I type this, it feels like we've discussed this type of approach in some other challenging cases we've wrestled with in the past (Lydia's sync field case, e.g.? Maybe something with arrays and domains?) which makes me wonder if we keep putting off the inevitable in some way...
2) Alternatively (and, I think, nearly equivalently, but more complicated and resulting in potential code blow-up), we could create clones of the initializer:
proc init(val: int) {
super.init(val);
this.z = this.x + this.y;
}
proc init(val: int, z: real) {
super.init(val);
this.z = z;
}
But I think the problem with this approach is once there's not just one field in the child but several, we get an exponential number of initializers. So having written this, I think case 1 would be the way to proceed, if we did want to consider this in the future.
I believe the stance we took in the past was "if the generated all-fields initializer can be resolved against, then let it go through". I don't remember the full details (I can check my notes when I get back in the office), but it looks like the behavior in your test is "since there is an explicit parent initializer, I can make no assumption about what arguments it has or what their particular relationship to the inherited fields are, so I cannot insert arguments into the child initializer beyond those for fields defined explicitly on it". I would have to take a look at the generated AST to see what the body tries to do with them.
Your strategy 1 sounds both appealing and familiar - I don't think we can perform strategy 2 due to the interplay with default values and how that would potentially lead to resolution issues, in addition to your point about an exponential number of them.
I just ran into this issue. Is there any concern with an intermediate approach where the child type gets all (only?) the user-defined initializers from the parent? For example, the following code works and is what I would have expected:
class Parent {
var x: int;
var y: real;
proc init(val: int) {
this.x = val;
this.y = val :real;
}
}
class Child: Parent {
var z = x * y;
proc init(val: int) {
super.init(val);
}
}
proc main() {
writeln(new Parent(5));
writeln(new Child(5));
}
```terminal
{x = 5, y = 5.0}
{x = 5, y = 5.0, z = 25.0}
I think this is equivalent to Brad's strategy 2a in https://github.com/chapel-lang/chapel/issues/8232#issuecomment-429068318 where only this initializer is defined:
```chapel
proc init(val: int) {
super.init(val);
this.z = this.x + this.y;
}
and appears to be compatible with strategy 1.
@BryantLam: To make sure I'm understanding, I think you're suggesting that the compiler should have / could have inserted the child initializer:
proc [Child.]init(val: int) {
super.init(val);
}
in the event that you hadn't done so explicitly in your code, is that correct?
@benharsh: What are your current thoughts on this issue (and Bryant's most recent suggestion) having lived in the initializers world more in the past 12 months than most of us?
That's right. I'm suggesting that the compiler could add a straightforward super-initializing child initializer for every user-defined parent initializer. In a way, the child is "inheriting" the user-defined initializers from the parent, which I imagine is the behavior most people would expect.
Any user-defined child initializer could be additive to [these] compiler-generated ones.
This strategy should be unified with #8136 whether that be opt-in or opt-out.
Any user-defined child initializer would be additive to the compiler-generated ones.
This part seems surprising to me if I'm understanding correctly. Usually the creation of a user-defined initializer overrides the compiler-generated one, at least by default (i.e., modulo any support added in #8136). So if I created a parent class and overrode its default initializer by providing my own, the parent class wouldn't have a compiler-provided initializer, and I'd think that the child class shouldn't get one either since I'd done something to lock away how you can initialize its parent's class fields. If the child class did get a compiler-generated initializer, that would essentially make them directly available again (unless we somehow base the compiler-generated child initializer on the user-provided parent one which is what I was trying to wrestle with in my original comment).
For example, in your example above, your creation of Parent.init() essentially prevents me from setting x or y individually/independently. But if the compiler created its normal child class initializer in addition to the one you proposed, that would look something like:
proc Child.init(x: int, y: real, z = x*y) {
this.x = x;
this.y = y;
this.z = z;
}
permitting me to set x or y independently, and breaking the invariant you'd established by creating your own initializer.
Usually the creation of a user-defined initializer overrides the compiler-generated one, at least by default (i.e., modulo any support added in #8136).
That's a fair criticism. I'm okay with not having the additive child initializers because of that statement for the sake of consistency.
Edit: To be clear, I was suggesting that any user-defined initializers in the child would be additive to only the "inherited" compiler-generated initializers from the parent and not all the default compiler-generated initailizers. The intent is to eliminate copy+pasting the user-defined parent initailizers if I had to create a user-defined child initializer for some reason.
In the end, the behaviors from #8136 is what I would actually prefer for the additive initializers, if reasonable.
Most helpful comment
(I know you know, but for posterity) note that this is brought up as a possible extension in the initializers chip (see the end of this section)