Synthesizing default values for the memberwise initializer

Hello Evolution,

Following my post about the State of the Memberwise Initializer, I wanted to push forward in solving this limitation that the memberwise initializer has.

Synthesize default values for the memberwise initializer

Introduction

This proposal aims to solve a simple outstanding problem with the way the Swift compiler currently synthesizes the memberwise initializer for structures by synthesizing default values for properties with default initializers.

This is mentioned in the "State of the Memberwise Initializer" forum post: here

Motivation

Currently the Swift compiler is able to synthesize a fairly basic memberwise initializer for structures.

struct Dog {
  var age: Int
  var name: String
}

The compiler is able to synthesize a memberwise iniailizer for this structure which simply looks like this:

init(age: Int, name: String)

But, lets say we want all dogs to have a default value of 0 for the age:

struct Dog {
  var age: Int = 0
  var name: String
}

A user might naively try using this default value when constructing their Dog instance:

// I just want to set the name of Dog, sparky is a newborn
let sparky = Dog(name: "Sparky")

To their surprise, they can't. missing argument for parameter 'age' in call. Using the compiler synthesized memberwise initializer has turned to become a nuisance rather than a nice removal of boilerplate. In many cases the user may optionally just define their own initializer with a default value for the age parameter.

struct Dog {
  var age: Int = 0
  var name: String
  
  // This is defined because the Swift compiler can't generate default values for properties with an initial value
  init(age: Int = 0, name: String) {
    self.age = age
    self.name = name
  }
}

Proposed solution

I propose simply doing the obvious and synthesizing default values for properties with default initializers in the memberwise initializer. Simple code like the following will simply work:

struct Dog {
  var age: Int = 0
  var name: String
}

// This now works
let sparky = Dog(name: "Sparky") // Dog(age: 0, name: "Sparky")

Detailed design

This change does not alter the requirements needed to synthesize the memberwise initializer, but rather if we can synthesize the memberwise initializer, also synthesize default values for properties with default initializers. Note that we can only synthesize values for variables that have declared default initializers and not constants.

Source compatibility

This is a purely additive feature, thus source compatibility is not affected.

Effect on ABI stability

This feature does not alter ABI, thus ABI stability is not affected.

Effect on API resilience

As the memberwise initializer is only synthesized as an internal initializer, this feature does not affect API resilience.

Alternatives considered

We could simply not do this and save this proposal for a solution much larger in regards to fixing more problems the memberwise initializer has. The downside is that we put off obvious changes like this for much longer because of wanting to solve a bigger problem. I agree we should solve the bigger problems, but by solving problems like this it aids in the solution of the larger problem.

20 Likes

Not quite clear from the proposal:

Given

struct Dog {
    let age = 0
    let name: String
}

is Dog(age: 2, name: "Rusty") valid?

EDIT: Answer retracted. I misunderstood the question. See @Alejandro's answer below.

I imagine in that case the compiler is generating the equivalent of

  // This is defined because the Swift compiler can't generate default values for properties with an initial value
  init(age: Int = 0, name: String) {
    self.age = age
    self.name = name
  }

So Dog(age: 2, name: "Rusty") would be valid.

No. With the way constants are handled, we cannot alter their storage if they already have a pattern binding. We can only synthesize a default value for variables and not constants.

Note that we can only synthesize values for variables that have declared default initializers and not constants .

Swift initializers are already quite complicated, and I'm extremely skeptical towards adding more complexity - but this proposal does neither add new syntax or attributes, nor surprising rules, so I think it's a good addition.

Is it too much to treat this as a bug? IMO this isn't changing behavior as much as SE-0018 was going to. And while I would also like to see that passed eventually, this feels like just fixing something I would expect to work.

2 Likes

This feature is like the default values of struct in Golang language. I think it good to have this in Swift.

yes, this needs to happen :slight_smile:

This seems like an addition that just makes logical sense. As @nuclearace already mentioned, it feels like a bug that this isn't supported already.

2 Likes

Ah, my apologies, I missed that sentence. That seems like a pretty big limitation of the proposed behavior, then. A handwritten initializer can contain a default value for a constant. Can you comment on the reasons for this?

Additionally, the implication of "only variables" seems to be that the property is actually set by the default and then again by the value passed in to the initializer. Is that correct? If so, isn't there potential for surprising behavior if the default is not a simple constant but has some side effect? (Not saying that's common or even necessarily a good idea, but it's valid code.)

I wouldn't want the behavior for var also to work with let even if possible:
When I see let x = 5, I expect that x will always be 5 - and that won't be true anymore if the synthesized init would take the assignment to be just a default value.

This is true of the current model, although the core team was receptive to the idea of changing this in their feedback to SE-0018. If it were changed, the model would be "immutable after initialization". Assignment in the declaration would be treated as a default initialization that is not used if an initializer provides a different value for the constant.

I understand that you want to keep this proposal simple so I think it's ok to leave that enhancement off for now, but supporting this is a viable future direction.

As the author of SE-0018, I would really like to revisit the topic in a more comprehensive manner someday, but I think this is a good intermediate step that would not conflict with any direction we might eventually take. It fills an obvious gap in the current initializer synthesis and should be straightforward to implement.

2 Likes

Not quite. A handwritten initializer can contain a default value for a parameter that will be assigned to a constant property. But that constant property can only be assigned if you don't specify a value as part of its declaration.

Fair point, thanks for noting that. It does not really seemcontradictory to what I was asking, though, since the proposal is essentially making he shorthand property initialization syntax equivalent to a default argument.

Again, not quite. A default argument is simply a value for a parameter that the compiler inserts for you at the call site, if you don't specify it explicitly in the code. What the function does with that parameter, whether it's a default value or not, is not relevant.

This proposal is to extend the synthesis of initializers to infer that preinitialized vars should be given default values. This matters because the synthesized initializer actually has the equivalent of self.param1 = param1 in its body. That is, the value of each parameter is used to initialize a property of the same name. However, this doesn't work for constants. The synthesized code would (currently, see Mathew's post above) not work. This means adding a default value for the related parameter makes no sense (at this time).

1 Like