Spin-off from #16554. Arguably related to #5054.
Suppose a type defines an init= method, e.g.:
record R {
var x: int;
proc init=(rhs: int) {
this.x = rhs;
}
}
proc =(ref lhs:R, const ref rhs:int) {
lhs.x = rhs;
}
It is already the case that the init= method can allow initialization from a different type, e.g.:
var x: R = 1;
Should the existence of the init= method also allow one to call a function accepting the same type by in intent?
f(1);
proc f(in arg: R) { }
Argument that the call should not be allowed:
R.init=(rhs: int) was not necessarily trying to indicate that functions accepting an R can alternatively accept an int. Would we need an explicit keyword like C++ does? In other words - a user might want init= and = to work across types even if they do not desire implicit conversions on a function call.Arguments that the call should be allowed:
in argument is analogous to variable initializationinit=(rhs:int) is different from init(arg: int) and so this won't come up with constructors for use with new. Second, in intent is relatively rare in Chapel code in practice. I see the choice here is between:
init= only to variable declarations syntactically present in the source code, andinit= to all variable-initialization-like situations.Without seeing real-life examples I prefer "go all the way".
"Go all the way" option implies that init= defines an implicit coercion. An interesting question is whether it should apply to a const ref formal. The answer should be the same as whether an int is passable by const ref to a real formal.
An interesting question is whether it should apply to a const ref formal. The answer should be the same as whether an int is passable by const ref to a real formal.
This seems like a side issue to me but I don't see how that could possibly work. A const ref formal should preserve the identity of the original value (so that if, for example, it is modified by another call, the const ref formal sees the change). Otherwise it is not const ref but rather in (or something). Anyway, I don't see how this identity property could be preserved with a int coercing to a real.
w.r.t. the OP, given our recent re-interpretation of in arguments as being symmetric with variable initialization, it would seem odd to me to treat them differently w.r.t. init= operations. Or at least, I feel like I'd want to see a good argument for why it ought to be different from the language design / orthogonality perspective (i.e., I don't think "it would make the compiler more complex / slower" is the right rationale for this decision).
I thought that the C++ problem motivating explicit stemmed from its not using distinct syntax/naming for copy initializers vs. plain-old initializers (i.e. those intended to support calls like new ...); and that in Chapel since we have init() vs. init=(), the chances of one unintentionally opting into something that they didn't mean isn't so much of an issue. Specifically, the classic C++ case that I come back to (written as Chapel) is when a user wrote:
proc MyVector.init(size: int) {
// allocate a size-element vector
}
and found that it inadvertently supported:
var v: myVector = 10;
or
proc foo(in v: myVector) { ... }
foo(10);
Whereas in Chapel, in order to support those forms, I'd have to define:
proc myVector.init=(size: int) { ... }
which is a very different beast, in which my intention would be to enable things like:
var v: myVect = 10;
As a good motivator, I think the init= that is defined for a sync int or bigint that takes an int and turns it into that type is an interesting case to consider. And it seems convenient that if I wrote:
proc foo(in x: bigint) { ... }
proc bar(in x: sync int) { ... }
I could say:
foo(45);
bar(33);
rather than needing to say:
var x: bigint = 45;
foo(x);
var y: sync int = 33;
bar(y);
or:
proc foo(in x_arg: int) {
var x: bigint = x_arg;
foo(x);
}
proc bar(in x_arg: int) {
var x: sync int = x_arg;
bar(x);
}
or:
foo(new bigint(45));
// foo(new sync int(33)); // this isn't actually supported today
This last case is interestingly entangled with the challenges I had disabling implicit reads/writes of syncs in that the compiler-generated initializers that current take sync int arguments by default no longer make sense. I was taking the approach of having the compiler special-case proc init(in x: sync int) into proc init(in x: _desync(sync int)) which is essentially proc init(in x: int). But if we could keep it as proc init(in x: sync int) and be able to pass int into it by virtue of the init= overload, this would remove a nice special-case (while also permitting people to send int into a compiler-initializer for a bigint field as well).
w.r.t. Vass's comment, I agree with @mppf that init= shouldn't be applied in the case of a [const] ref intent...
init=shouldn't be applied in the case of a[const] refintent
The rationale makes sense, thank you.
Let us keep it this way.
Won't this cause conflicts in the case where there's an overload of the function for the original type? E.g.
proc foo(in x: int) {...}
proc foo(in x: R) {...}
@lydia-duncan - it would make both of the foo in your examples candidates, but function overload resolution would choose the x: R version or the x: int version for foo(myR) and foo(1) respectively according to the rules here : https://chapel-lang.org/docs/master/language/spec/procedures.html#determining-more-specific-functions
Won't this cause conflicts in the case where there's an overload of the function for the original type?
It could be an ambiguity, or we could decide to prefer the precise type match over the one that requires a copy initialization.
we could decide to prefer the precise type match over the one that requires a copy initialization.
that is already the rule (the spec would call it "implicit conversion" vs. copy init though)
The relevant bit is this
"If Ta and Tx are the same type and Ta and Ty are not the same type, Mx is more specific"
and also "If there is an implicit conversion from Tx to Ty, then Mx is more specific"
However it is certainly the case that we would rely on function disambiguation in more cases if we allow things like f(1); in the original post.
See https://github.com/chapel-lang/chapel/issues/15838#issuecomment-708463962 - depending on how we handle non-record types, a cross-type = might cause us to allow implicit conversions for an in intent argument.