We've run into a couple of situations where recognizing or failing to recognize a function as a copy initializer caused problems. This issue is an attempt to summarize the positions and reach a consensus.
Only initializers that are explicitly marked in some way should be considered copy initializers
Initializers with a single generic argument are allowed to be copy initializers, but if an initializer with a single generic argument is not intended as a copy initializer, it must be given a where clause that excludes the copy initializer case, or an error may occur at compilation time even if no copy is made.
Example 1: the initializer with a generic argument is treated as a copy initializer, but it does not work as one. An error is generated when trying to resolve it as if it was a copy initializer
record Foo {
var x: int;
proc init(xVal) {
x = xVal; // Error on this line, because x is of type int, while xVal is of type Foo.
super.init();
}
}
Example 2: The where clause prevents this initializer from being treated like a copy initializer.
record Foo {
var x: int;
proc init(xVal) where (!xVal: Foo) {
x = xVal;
super.init();
}
}
An explicit type for the argument is required for the initializer to be recognized as a copy initializer
record Foo {
var x: int;
// No attempt is made to treat this as a copy initializer
proc init(xVal) {
x = xVal;
super.init();
}
// So the user has to write this to specify a copy initializer
proc init(other: Foo) {
x = other.x;
super.init();
}
}
It is my understanding that we would prefer Option 2.
My gut reaction is to prefer Option 2 and further to assume
that we can, when priorities allow, introduce a helpful warning
if a given program “happens” to compile and a helpful error
message when it does not.
Michael and I were able to hammer out a branch that generates an additional message about copy initializers for Option 2, example 1, so that it isn't as confusing to see a call the user did not explicitly insert.
Reading CHIP 10 https://github.com/chapel-lang/chapel/blob/master/doc/rst/developer/chips/10.rst#copy-initializers gives me the sense it means Option 3 - I think we should update it if we go with Option 2.
My intuition would be that the copy initializer for a record R should be any initializer that resolves correctly for myNewR.init(myR) (where myNewR and myR are both instances of R). I think this is option 2?
I could potentially see going with option 1, but would probably need a compelling counterargument to option 2 and a good naming proposal (copyFrom doesn't excite me—seems too different from init).
(Just to weigh in on all the options, option 3 feels a bit too strict to me in that if I wrote a legal copy initializer without supplying the type, it seems a shame not to permit it to work. I'd also expect that I could have a copy initializer in which there were extra (defaulted) arguments that might be used in cases where I invoked it explicitly via a new call while still permitting it to work in a compiler-generated call—see issue #7932).
To be clear, the copyFrom idea in Option 1 was referring to this:
record Foo {
// declares a copy initializer
proc init(copyFrom) {
...
}
}
Oh, thanks for clarifying, I did misunderstand that it was referring to the naming of the argument rather than the routine itself. This makes me like it far less than options 2 or 3.
Right, but Option 1 is intended to include other possibilities along those lines, such as naming it 'copy' rather than 'init', or adding a 'copy' decorator keyword or ...
OK, then consider my enthusiasm for option 1 somewhere below 2 but above 3 where my excitement is going to vary greatly depending on which choice we're talking about (e.g., pragma: bleah. name: acceptable, depending on the name. new keyword: seems like overkill given that initializers didn't even warrant one. ...:???)
Do our favorite languages to learn from suggest any of these options over the others?
C++: Option 3 (but it would allow default arguments)
D: I'd call it Option 1 - uses a postblit idea (which is always declared this(this) )
C#, Swift: has record-like types, but you can't write a copy constructor for them
Rust: Copy constructor can only be explicitly invoked & the interface for it has one argument
Java, Scala: no record-like types as far as I know
Here's the C++ program I used to figure out what C++ does:
#include <cstdio>
template<typename T>
struct GenericStruct {
T field;
template<typename U>
GenericStruct(const U& other) {
printf("In GenericStruct generic constructor\n");
field = other.field + 1;
}
/*GenericStruct(const GenericStruct<T>& other) {
printf("In GenericStruct specific copy constructor\n");
field = other.field + 1;
}*/
GenericStruct() {
printf("In GenericStruct default constructor\n");
}
};
int main() {
GenericStruct<int> a;
a.field = 1;
GenericStruct<int> b = a; // should copy-construct
printf("a.field is %i\n", a.field);
printf("b.field is %i\n", b.field);
}
To be certain we understand the consequences of this decision: I had to make the following update to a couple of chameneos implementations, because their initializer was recognized as having the right argument set up but was not actually intended as a copy initializer. Are we still okay with this outcome?
In person this afternoon, I said I was fine with these chameneos versions needing an update for this purpose, though suggested switching to a formal array argument type (c: []) rather than a where clause to do the filtering. It'd be good to keep a list of other cases like this as we come across them.
C#, Swift: has record-like types, but you can't write a copy constructor for them
I'm probably just forgetting, but how do they get around this? (or: How do they avoid the "Can't write interesting record patterns" problem that Chapel prior to initializers had?)
I created this gist to track initializers that had to be adjusted to not be considered copy initializers
C#, Swift: has record-like types, but you can't write a copy constructor for them
I'm probably just forgetting, but how do they get around this? (or: How do they avoid the "Can't write interesting record patterns" problem that Chapel prior to initializers had?)
I think the short answer is that they rely upon the garbage collector that these languages have.
E.g. for the string type in Swift:
Although strings in Swift have value semantics, strings use a copy-on-write strategy to store their data in a buffer. This buffer can then be shared by different copies of a string. A string’s data is only copied lazily, upon mutation, when more than one string instance is using the same buffer.
If our string type had this behavior, we'd have to have some strategy to reclaim the memory. That could be reference counting or a full-on garbage collector.
Any objections to closing this issue, since the change that prompted it has been merged and we seem satisfied with the direction?
I'd be fine with closing this issue. As I've continued translating copy initializers, I suspect I may open other design-related issues.
To foreshadow, one that I've been thinking about for the past 24 hours or so is "If a user provides their own (non-copy) initializers, should that squash the creation of the compiler-provided copy initializer by default, as it does the compiler-provided default initializer? My rationale is that in a case where I was providing initializers for a complex type, the compiler-provided initializer was woefully naive, but is what was used. If instead I'd gotten an error saying something like "copy initializer needed here, but not available", I would've been able to deal with the issues in a more straightforward way. If there were a way to say "please make the compiler-provided copy initializer available even though I've written by own initializers" (as I've also wanted for default initializers), all the better for cases where it would be sufficient.
Wasn't that already covered in #8065?
Thanks for the reminder, yes. I guess what I'm saying is that my experience thus far suggests that maybe we made the wrong decision. I'll put this text over there and would be inclined to re-open it unless you object.
Definitely put the text over there. I would rather not change our minds immediately with new information, though, because it feels like we go back and forth on decisions a lot and I am finding it really exhausting
I'm not suggesting changing our minds immediately, I'm only suggesting re-opening it to indicate that something that we thought was decided perhaps was done with insufficient experience / information. The whole point of converting existing code, and particularly module code, over was to get experience with our initial draft ideas. If we're not willing to revisit decisions based on that experience simply because we don't want to change a decision, that suggests we really weren't really open to learning we were wrong...
Over on issue #8242, I've just wondered out loud whether copy initializers (or really, any initializers that should work in a context where = is used to express the initialization) should be declared as:
record R {
proc init=(rhs: R) { // note the name 'init=' rather than simply 'init'
...
}
}