Swift does have a rule, independent of anything discussed here, that var x: Int? is equivalent to var x: Int? = nil. (Compare with let x: Int?, which does not have the same behavior.) I don't think we should treat var x: Int? differently from var x: Int? = nil for the purposes of memberwise initializers.
(People have talked about removing this behavior from var x: Int?, since "mutable stored Optional" is the only kind of declaration that gets an implicit default value. But I think that should be a different discussion and one that's applied consistently to all such declarations, not just the projection of the initial value into the memberwise initializer's default arguments.)
I do want to touch on what Jordan and the others regarding special types of default values for variables. The intention of this proposal is not to change the mental model when considering default values and the memberwise initializer. That being said, with Jordan's example:
func zeroWithSideEffects() -> Int {
print("hello!")
return 0
}
struct IntWrapper {
var value: Int = zeroWithSideEffects()
}
let x = X()
the implementation only prints hello! once. A lot of what's happening is implementation detail, but in short, the default initializer is still present (because all properties are default initializable) in this case and the type checker favors that initializer over a memberwise one with full default values. Even without considering the default initializer, using the memberwise initializer still only prints hello! once. The case where it prints twice is where the user explicitly writes out an initializer and opts out of both default and memberwise initializers for the struct.
Regarding optionals, the implementation implicitly assigns nil as a default value because of the special default initialization of optionals. I didn't want to special case the memberwise initializer as Jordan points out.
Your example only shows a single property. What happens in the case where there are multiple properties and some do not have default values? The current behavior for a manually written initializer will perform side effects twice, as in the following code:
func zeroWithSideEffects() -> Int {
print("hello!")
return 0
}
struct IntWrapper {
var value: Int = zeroWithSideEffects()
var other: Int
init(value: Int = zeroWithSideEffects(), other: Int) {
self.value = value
self.other = other
}
}
// prints âhello!â twice
let x = IntWrapper(other: 42)
This still only prints once. The memberwise initializer is lowered a bit differently than say a handwritten initializer. It might be that this could cause a source of confusion because technically identical initializers behaving differently, and I see the argument against this. We might be able to change the behavior of an explicit initializer (source compatibility issues maybe?), but we can also theoretically make the side effect occur twice to be consistent with an explicit initializer (but it seems strange to introduce a double side effect).
Grr. I didn't mean to get mixed up with the no-arg initializer in my example, but I have a strong distaste for the compiler doing something you can't write explicitly yourself, particularly if we ever get an "expose the memberwise initializer as public" annotation. I'm against this proposal if it does not evaluate twice if manually writing the equivalent thing does evaluate twice.
I hadnât thought about the relationship of this proposal to the default initializer deeply enough. It makes sense that it would adopt behavior that matches that initializer. Doing anything different would be a breaking change since it combines the default initializer and the memberwise initializer into one initializer with default arguments. This is a nice simplification of Swiftâs initializer model.
I generally agree with your sentiment, however as I mentioned earlier, I think what most people want is for the side effect to only be performed once. IMO, the fact that we canât suppress the initial default value assignment when a custom initializer wants to initialize a property differently is very subtle and can result in suprising behavior. It also results in minor (but unnecessary) runtime overhead. I think it would be possible to solve these issues if / when we revisit SE-0018 and the initialization model more broadly.
In the meantime, I agree that the difference in behavior between the synthesized initializer and a manually written initializer would be somewhat unfortunate. On the other hand, it is not an entirely new problem - it only expands an existing difference in behavior that already exists for the default initializer:
func zeroWithSideEffects() -> Int {
print("hello!")
return 0
}
struct IntWrapper {
var value: Int = zeroWithSideEffects()
init(value: Int = zeroWithSideEffects()) {
self.value = value
}
}
// prints âhello!â twice
let x = IntWrapper()
The important factors IMO are:
this proposal provides significant convenience for a very common use case
the difference in behavior between synthesized and manual initializer already exists in a limited form
the synthesized initializer will provide semantics people probably desire for their initializers but cannot write manually
there is a clear path forward from this proposal to a more complete revisit of the initializer model and SE-0018
I think all of these factors combined significantly outweigh the issue of synthesizing code that cannot be written manually. I think it would be pretty unfortunate to reject this mostly straightforward and broadly supported incremental improvement on the grounds that we canât manually write initializers with identical semantics to the synthesized initializer. Especially so, given that we do not want to write these initailizers manually and we want the synthesized semantics, not the semantics we can write manually.
I agree with Jordan that this is a negative, and a strong one, not a positive. What you illustrate about the current situation can be written manually by adding init() { }. It would be unprecedented to have an automatically synthesized initializer that is unutterable, particularly since such an initializer can never be exposed publicly. Moreover, since it's unutterable, it's also wildly unteachable: now we're saying that not only can't you see the body anywhere, we can't even show you the declaration.
I agree that itâs a negative as well. But the root cause of the negative is in the existing semantics of default values and how they interact with manually written initializers. ¸I would very much like to see this solved in a more comprehensive way! But I think it would be really unfortunate to reject an incremental step in the right direction unless it was going to get in the way of a more comprehensive solution. I donât see that being the case here and I think this proposal would deliver significant immediate benefit to many developers.
The existing behavior where side effects occur twice is wrong. It is not what people expect or want, and it should be considered a known bug.
It deeply saddens me to see that some people here are so strongly bound to an ideal of âconsistencyâ that they would prefer to make code consistently do the wrong thing, even to the extent that they oppose adding a convenience feature that would do the right thing.
⢠⢠â˘
Luckily, there are two separate ways to view the proposed addition as entirely consistent.
First, we can declare the existing duplicated-side-effects behavior a known bug, which we fully intend to fix in the future. That means the side effects should only occur once, and the proposal is consistent with that goal.
Second, we can declare the proposal as being entirely syntactic sugar. That is:
struct Foo {
var x: Int = xValue()
var y: Int = yValue()
}
Actually desugars to:
struct Foo {
var x: Int
var y: Int
init (x: Int = xValue(), y: Int = yValue()) {
self.x = x
self.y = y
}
}
With this model, the side effects only occur once, and the equivalent code can in fact be written today.
⢠⢠â˘
No matter how we look at it, the proposed behavior is exactly what people want, exactly what they expect, and exactly what we should implement.
Is the problem being addressed significant enough to warrant a change to Swift?
Yes
Does this proposal fit well with the feel and direction of Swift?
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
Nothing too surprising or out of sorts
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
I read the proposal and have considered the feature before
The only reasonable argument against this that I can think up is that we don't know how it will fit with the possible feature that would allow us to control the visibility of synthesized initializers if we still hope to add it.
Neither of these views would be consistent or source compatible. The spelling var x = f() cannot be pure sugar because it is evaluated regardless of the initializer that is called. Similarly, the behavior today cannot be described as a bug because this notation is the only way to evaluate and assign the return value of f() to x regardless of the initializer that is called. This has many crucial implications, such as the answer to the question, âWhen has my value been fully initialized?â
The behavior today is exactly what I would expect and continue to want from var x = f(). And if the consequence of that is that an initializer with a default value causes f() to be evaluated twice for lack of a notation otherwise, then so be it.
(Note that, if f() has no side effects, there is of course no need for it to be evaluated twice. But like copy-on-write for value types, that should strictly be an optimization with no user-observable effects.)
There is already a way to spell what you are describing as âwhat the user wants.â It is to choose not to use the notation that explicitly and exclusively means âevaluate f() no matter what initializer is used.â
Only someone who is intimately familiar with the Swift compiler can write such a paragraph.
But we also have to deal with the Principle of least astonishment. Those double evaluations, even though consistent and perfectly rational, according to the current Swift model and implementation, are unlikely to be expected, even by seasoned developers.
I'd like to be even more assertive: there is a high chance that when someone discovers this double evaluation, it is because of some problem.
This is the context of the posts you were replying to: this double evaluation is weird.
It looks like we are building funny interview questions for the post SE-0242 world:
TELL ME, young candidate, are all those three structs API-compatible? ABI-compatible? Would your answer change if the name property were optional? Do you sometimes feel uncomfortable during refactoring? If so, what do you think could be fixed in the Swift initialization process?
struct S {
var name: String
init(name: String = someComputation()) {
self.name = name
}
}
struct S {
var name = someComputation()
init(name: String = someComputation()) {
self.name = name
}
}
struct S {
var name = someComputation()
}
(I'm sorry if any reader has tried to answer this interview question, because it is a real chore.)
This is a real trouble, I agree.
To conclude, the double evaluation we are talking about should be no threat for this proposal. We're just having a side discussion.
I'm wondering if the nullary initializer that we synthesize when all properties have a default would still be needed, since with this proposal its equivalent to an invocation of the memberwise init with no arguments.
Also I'm assuming that the definition of 'has default' here always includes Optional types? Today, the following will default-initialize the Optionals to nil:
Is the "double evaluation" considered a longstanding limitation of the language initialisation model or a feature? If considered a feature, is there a well defined use-case?
I found the behaviour surprising, since it means that an expensive object could unexpectedly be constructed twice. Think for example a struct that with a default NumberFormatter.
Similarly, the behavior today cannot be described as a bug because this notation is the only way to evaluate and assign the return value of f() to x regardless of the initializer that is called
Could the behaviour be "relaxed" to say that the compiler is allowed to optimise away the initial variable default assignment, if it is reassigned in the initialiser, and there is no visible effect (property access) prior to that?
No, thatâs only if the default value is nil. Otherwise, you could write something like this only if the value is otherwise fully initialized and the type of the member in question is Equatable.
Itâs precisely because itâs rational and perfectly consistent that it can be expected at all. By contrast, not evaluating the value twice is something that can be explained only by someone intimately familiar with the compiler. Itâs not expressible in the language by any spelling and requires compiler magic.
I disagree. At present, the proposal does not specify the behavior of this scenario, and the implementation currently behaves in a way that cannot be spelled explicitly. I agree with Jordan that it must be specified, and that the specification must be double evaluation, for the reasons outlined above. Without resolution of this I would find this proposal to be problematic.