SE-0242: Synthesize default values for the memberwise initializer


(Davide De Franceschi) #61

I'm not sure I follow:

But the default value is nil. It will always be nil, as the type of the property gets wrapped in an Optional, and some means "do use this value" and none means "keep the default value".

It is, as it happens only for properties with a default value?

Why is this constraint required?


(Matthew Johnson) #62

Evaluating this regardless of the initializer that is called is precisely the behavior that is questionable. The implications for the initialization model are the reason we cannot fix it without revisiting SE-0018 and the initializer model more broadly.

What users often actually want is to declare a “default value” that is used if no other value is specified by the initializer. Usually when an initializer does specify a different value users do not want the “default value” expression to be evaluated at all. Not only is this more aligned with what people intend regarding side effects, it is also compatible with having a “default value” for let properties.

Assignment to the property is the only existing syntax that could support the desired behavior. At one point in the SE-0018 discussions I suggested we could have an alternate syntax for “default value” expressions that would only be used in memberwise initailizers and would not be evaluated otherwise. @Chris_Lattner3 was not a fan of this duplication and instead suggested that it might be better to revisit existing semantics. The core team even indicated support for some “default value” solution that would support let properties, presumably using the existing syntax.

So I believe changing the semantics of the existing syntax is on the table whenever the topic is revisited in a more broad form. As far as I can tell, this proposal is aligned with the semantics that we would move to. You are of course welcome to have an opinion that it would be a bad direction, but that would only be your opinion. Others are welcome to (and clearly do) have different opinions.


(Matthew Johnson) #63

I had been thinking it would be an unnecessary overload that could cause problems and assumed it would replace that initializer. In thinking deeper though, it would behave differently when the initializer is used as a function value rather than being invoked since default arguments are not used in that case.

With that realization, I’m not sure what the answer is. It would be a breaking change to remove the default initializer, but it does still seem undesirable to have both.


#64

They would both be consistent. Each of the two options reflects an internally-consistent perspective. And the second one (pure sugar) would also be source compatible.

On the other hand, if we declare the unwanted behavior to be a bug, then yes, fixing it would not be source-compatible. That is kind of the point of bug-fixes: if the buggy behavior were not observable, there would be nothing to fix.

Yes it can be pure sugar, as I demonstrated.

Furthermore, we are talking specifically about structs with zero user-defined initializers, meaning the *only* initializer available is the implicit default one. There are no other initializers involved.

I literally described it as a bug, therefore it can be described as a bug. QED.

And again, there is only one initializer involved, which is synthesized by the compiler. So the answer is that the value has been fully initialized when it is returned from the compiler-synthesized initializer.

I already provided an example of how to spell the proposed behavior explicitly. Please stop with the false statements.


(Xiaodi Wu) #65

Evaluating regardless of the initializer used is the meaning of the syntax in question. I agree that it would be nice to have the feature you describe, but I simply don’t see how it can supplant the meaning of this syntax: it would have to be spelled differently because any such change would be wildly source breaking.

I don’t think it would be a bad direction; in fact, if designed from scratch, I think it would be a superior design. However, because it would be source breaking, I do think it is an impossible direction for evolution to take.

On that basis, I would be opposed to the current proposal incorporating an unutterable behavior.


(Xiaodi Wu) #66

What we are discussing regarding double evaluation is orthogonal to the issue with Optional members. We are taking about functions with side effects being evaluated twice:

var global: Int = 0
func f() -> Int { global = global + 1; return global }

struct S {
  var x: Int = f()
  var y: Int
  init(_ x: Int = f()) { self.x = x; self.y = f() }
}

To avoid evaluating the function twice, you would have to test if x already has the desired default value. To do so, the object has to be fully initialized and the member has to be Equatable.


(Matthew Johnson) #67

Obviously source compatibility is a much bigger concern now than it was back during the SE-0018 discussions early in the Swift 3 timeframe so perhaps you’re right. At that time, having two sets of syntax with subtly different behavior was considered undesirable. I don’t think it’s clear that it would be acceptable today.

I think it’s an overstatement to call it wildly source breaking. The only actual breakage would be when people are actually relying on side effects being performed and the number of times the expression is evaluated changes in the new model.

Source breakage is still allowed, it just has to be very well motivated. It’s certainly conceivable that the “actively harmful” threshold could be met in this case. But that’s a discussion for another day.


(Xiaodi Wu) #68

I am talking about how we would spell the proposed behavior explicitly, when other initializers are involved. You are talking about how to reinterpret the meaning of var x = f() as sugar, which of course has to mean the same thing regardless of how many initializers are involved. I urge you to re-read my reply.


(Xiaodi Wu) #69

This cuts both ways though; the only observable difference we are discussing here occurs when people are relying on a function to produce a default value and when that function has side effects. By definition no one today is relying on that side effect happening only once, but there are inevitably people relying on it happening more than once.


(Xiaodi Wu) #70

It is, but whatever the case, I argue here that it is essential for the behavior of this proposal in all circumstances to be utterable.

If you believe it would be reasonable to change the behavior of var x = f() in a source-breaking way later, then naturally it would be acceptable to specify one behavior now for the default initializer consistent with the current state of Swift and to change that later. If such a change would be unacceptable, then a fortiori so would the larger proposed change to the meaning of var x = f().


(Davide De Franceschi) #71

I see what you mean now.
Yeah for what I was saying to work it'd need for the default values declared directly on the property to not be applied if a non-nil argument is passed, so something like self.c = c ?? c_default()

I had no comparisons in mind when I was suggesting that :)


(Matthew Johnson) #72

I agree with @xwu here that the simplistic desugaring @Nevin suggested is not viable. If memberwise initialization were that easy SE-0018 would have looked a lot different, probably would have been accepted, and we would not be discussing memberwise initialization today. There is a lot of subtly involved in making “default value” expressions, synthesized memberwise initialization and custom initializers all play well together.


(Matthew Johnson) #73

By definition nobody is relying on it happening more than once when calling a synthesized initializer. As has been discussed, the default initializer only performs side effects once. All code relying on the current synthesized memberwise intiailizer is providing all arguments explicitly as there are no default arguments provided.

Nothing in this proposal or a more comprehensive revisiting of SE-0018 will break existing manually written initializers. If we accept this proposal as-is, the path forward to more comprehensive memberwise initialization features would also not be source breaking as the behavior of the synthesized initializer would be to perform side effects once. If we require the proposal to be modified to perform side effects twice then moving forward would become more difficult as it would be a breaking change to modify that behavior.


#74

The point of DeFrenZ’s example is that the initial value var c: Double = cValue() would always happen, including side effects, just as it does today. Then the synthesized memberwise initializer would take an Optional parameter for c with a default value of `nil.

When called, DeFrenZ’s synthesized memberwise initializer would check to see if a non-nil value was passed in for c, and if so it would assign that value to c. This means the side-effects of cValue() would only occur once, unless the user manually wrote cValue() as the argument to the initializer, in which case the duplicate side-effects would presumably be intentional.

No, I am talking about how to interpret the behavior of the synthesized memberwise initializer.


(Matthew Johnson) #75

Clearly we have a difference of opinion on this point. I think both sides have been sufficiently articulated to let the core team exercise their judgment on the matter.


(Matthew Johnson) #76

This design is not viable. It changes the type signature of the initializer and assigns magic meaning to nil. Further, it leads to very confusing behavior when optional properties are involved. Consider this type:

struct Foo {
   var bar: Int? = 42
}

In order to support a type like this the signature of the synthesized initializer must be init(bar: Int?? = nil). The meaning of this outer nil is “use the default value” which is 42. In order to provide an argument that results in bar == nil a caller must specify .some(nil) as an argument. This is absurdly complex and makes the design a non-starter even if you are willing to accept the change in type signature and magic meaning of nil in other cases.


#77

I’m not advocating for it, just describing it. It is important that we all carry out these discussions based on facts and a good-faith effort to understand what other people are saying.

The person I responded to had completely mischaracterized DeFrenZ’s idea, so I wanted to spell it out clearly.


(Davide De Franceschi) #78

This is fair, and I did highlight that point when I explained the option

though I did use Optional as a quick prototype, but if this is the main issue I can see as a workaround to use something like (names just for example)

enum Defaultable<T> {
  case default
  case overrided(T)

and then the approach makes the init signature init(a: String, b: Int, c: Defaultable<Double> = .default)

I'm more worried about whether the semantics of not using the default should still evaluate it

Edit: I realise now that the enum approach is quite horrible as it would require each call to pass .overrided( when using it :disappointed: I'm not sure we want the same compiler magic that Optional has...


#79

I think this is the crux of the matter, and bears repeating.

+1

Today is your lucky day!

As it turns out, you *can* already write code that gives the same user-facing behavior as the proposal, evaluating side-effects only once. In fact, there are two different ways to do so, one of which involves a lot more boilerplate than the other.

First way:
Don’t use initial values with side effects, and instead manually define the initializers you want, putting the side-effectful calls into each of them as either a default value in the declaration or an assignment in the body, as appropriate. This is fairly repetitive and difficult to maintain, but it does work.

Second way:
When initial values have side effects, for every initializer that “wants to” use that initial value as a default argument, instead make two versions of that initializer, one with the corresponding parameter having no default value, and the other without that parameter at all. This of course multiplies the number of initializers by 2n, where n is the number of properties whose initial values have side effects, and is thus extremely verbose.

• • •

The first way entails a small amount of boilerplate: eschewing initial values in favor of default arguments in initializers.

The second way entails an exponentially large amount of boilerplate: duplicating initializers for every possible combination of arguments being present or absent.

But the fact remains, both of these approaches result in user-facing code with the same behavior as is proposed. Thus, the proposal can in fact be viewed as an extremely convenient way to reduce unseemly boilerplate.


(Michel Fortin) #80

I got a bit lost in all this discussion and I might have skipped over a post similar to what I'm about to write, but to me it sounds like you already can use the default initializer to sidestep default value evaluation. Take this example:

func getDefaultValue() -> Int {
	print("requested default value")
	return 0
}
struct S {
	var i: Int = getDefaultValue()
}
let s1 = S(i: 1)

It prints absolutely nothing because S(i: 1) magically skips evaluating the default value. If you try to manually write an initializer that does the same thing, it won't work:

init(i: Int) {
	// default values already evaluated at this point
	self.i = i
}

Synthesized initializers already behave in this particular way when you provide the initial values (not evaluating the default values) and it'd be surprising if this proposal was to change that.

Surely there is a need to have a better way to choose whether the default values get evaluated in manually written initializers, and I do have some ideas, but I don't see this as being very relevant here. Hopefully the proposal won't get derailed because of this.