Hello Evolution,
I want to discuss where we are with the current memberwise initializer for structures and where I wish to see this initializer evolve.
Refer to SE-0018 and SE-0018 Rationale. I do not speak for the author here, but I'm rathering pulling ideas and references from SE-0018 to formulate this post.
From here on out I'm going to refer to the memberwise initializer as MWI
Note that this is not technically a complete resurrection of SE-0018 because I will be stating my opinion on where we should go with the MWI.
Also, forgive me for restraining myself from saying "State of the Memberwise Initializer Address"
For those that don't know what exactly the MWI is, at the most basic level, the MWI is just a special compiler synthesized initializer for structures.
Given:
struct Dog {
var age: Int
var name: String
}
The compiler synthesizes a MWI for this structure which looks like this:
init(age: Int, name: String)
This initializer allows users to be able to initialize and instance of type Dog
like Dog(age: 2, name: "sparky")
. The MWI is a pretty fantastic initializer which frees the user of having to write boiler plate initializers which effectively do the same thing.
While the MWI is a fantastic gift from the compiler, it has a number of limitations which hold it back from being an even greater initializer. In SE-0018, it quotes Chris Lattner who spelled out some limitations that the MWI currently has:
The default memberwise initializer behavior of Swift has at least these deficiencies (IMO):
- Defining a custom init in a struct disables the memberwise initializer, and there is no easy way to get it back.
- Access control + the memberwise init often requires you to implement it yourself.
- We don’t get memberwise inits for classes.
- var properties with default initializers should have their parameter to the synthesized initializer defaulted.
- lazy properties with memberwise initializers have problems (the memberwise init eagerly touches it).
To this day, each of those 5 limitations are still in existance for the MWI. Below I have made examples excercising these limitations.
struct Dog {
var age: Int
var name: String
init(ageInYears: Int, legalName: String) {
...
}
}
let sparky = Dog(age: 2, name: "sparky") // Incorrect argument labels in call (have 'age:name:', expected 'ageInYears:legalName:')
public struct Dog {
public var age: Int
public var name: String
}
let sparky = Dog(age: 2, name: "sparky") // 'Dog' initializer is inaccessible due to 'internal' protection level
class Dog { // Class 'Dog' has no initializers
var age: Int // stored property 'age' without initial value prevents synthesized initializers
var name: String // stored property 'name' without initial value prevents synthesized initializers
}
let sparky = Dog(age: 2, name: "sparky") // 'Dog' cannot be constructed because it has no accessible initializers
struct Dog {
var age: Int = 0
var name: String
}
// Let's use the default age and just assign name
let sparky = Dog(name: "sparky") // Missing argument for parameter 'age' in call
In order to fully understand this one, it's best to understand how lazy properties work under the hood.
Given:
struct DogInfo {
var age: Int
var name: String
init(age: Int, name: String) {
print("Making dog info...")
self.age = age
self.name = name
}
}
struct Dog {
lazy var info: DogInfo = DogInfo(age: 0, name: "")
}
The compiler synthesizes something like this:
struct Dog {
lazy var info: DogInfo {
mutating get {
if let storage = info.storage { return storage }
let initialValue = DogInfo(age: 0, name: "")
info.storage = initialValue
return initialValue
}
set {
info.storage = newValue
}
}
private var info.storage: DogInfo?
}
Now that we understand how lazy properties work, where does the MWI come into this? The synthesized MWI for the example above looks something like this:
init(info: DogInfo)
Seems easy enough, lets try using it in action.
let sparky = Dog(info: DogInfo(age: 2, name: "sparky"))
// Making dog info...
What? I haven't even accessed the info property yet? What's going on? Well, what's really happening is that the value passed for info
is actually setting the info.storage
property with immediate execution of the expression. Effectively taking the "lazy" out of the lazy. (If I got anything wrong here, please feel free to correct me.)
SE-0018 was a good first step towards abolishing these limitations, but unfortunately the proposal was premature for its time. It was deferred for good reasons that the Rationale lays out. I think it's best to split SE-0018 into 2 parts for what it wants to accomplish: "Powerful MWI" and "Flexible MWI". I'm only going to discuss the first half, "Powerful MWI".
What in the world is a "Powerful MWI"? My idea of what a powerful MWI is is a future MWI in which it solves 4/5 current limitations. I'm excluding class support for now. Read Tweet #1 and Tweet #2 for why class support has a number of lingering questions to be asked/answered. Solving some of these limitations like default values for variables that have default initialers, access control, and disabling the MWI with presence of a custom init are all problems my "Powerful MWI" aim to solve. Other features like opting in/out for this MWI and opting properties in/out are out of scope for my "Powerful MWI" and should be reserved for "Flexible MWI".
This isn't exactly a proposal, but I simply wanted to get this topic discussed again because as we inch closer and closer to Swift 5, I think it might be worth to get some of these limitations solved. I'm curious what everyone thinks about my "Powerful MWI" idea.
I've actually already implemented a solution for Limitation 4 (No default values for variables with default initializers) here: apple/swift#19743. I would love to propose this to start solving these limitations if evolution deems that this is the way to go.