Thank you for the quick reply!
May I go a little bit off-topic explaining my mental model in order to understand its flaws and consequently the reasons behind SE-0258? It partly resembles Allow Property Wrappers with Multiple Arguments to Defer Initialization when wrappedValue is not Specified.
Borrowing a simplified example from the SE-0258 proposal:
@propertyWrapper
struct Field<Value: DatabaseValue> {
let name: String
private var cachedValue: Value?
init(name: String) { self.name = name }
var wrappedValue: Value { get set ... }
}
struct Person: DatabaseModel {
@Field(name: "first_name") var firstName: String
@Field(name: "last_name") var lastName: String
@Field(name: "date_of_birth") var birthdate: Date
}
Field
is a property wrapper that doesn't provide any .init(wrappedValue:)
or .init(wrappedValue:...)
initializer. Thus properties annotated with @Field
cannot be initialized with an initial value in the struct
and this implicitly means that their initial value should be provided by the property wrapper type itself (in this case, the property initial value would be the one fetched from the database). In line with @Andrew_Arnopoulos's proposal, I would expect it to be transformed into:
struct Person: DatabaseModel {
// no default values here
private var _firstName: Field<String>
private var _lastName: Field<String>
private var _birthdate: Field<Date>
var firstName: String { get set ... }
var lastName: String { get set ... }
var birthdate: Date { get set ... }
init() {
// default values here instead
_firstName = Field(name: "first_name")
_lastName = Field(name: "last_name")
_birthdate = Field(name: "date_of_birth")
}
}
Only an empty memberwise initializer should be generated since it's not possible to provide an initial value to @Field
wrapped properties.
Suppose now that also an .init(wrappedValue:name:)
initializer is available:
extension Field {
init(wrappedValue: Value, name: String) {
self.name = name
self.wrappedValue = wrappedValue
}
}
In this case we can either provide an initial value using =
or not, leaving its initialization in the generated memberwise initializer:
struct Person: DatabaseModel {
@Field(name: "height") var height: Double
@Field(name: "alive") var alive: Bool = true
// generated memberwise initializers:
// - case 'height' is using Field.init(name:)
init(alive: Bool = true) {
_height = Field(name: "height")
_alive = Field(wrappedValue: alive, name: "alive")
}
// - case 'height' is using Field.init(wrappedValue:name:)
init(height: Double, alive: Bool = true) {
_height = Field(wrappedValue: height, name: "height")
_alive = Field(wrappedValue: alive, name: "alive")
}
}
The property alive
has an initial value, so .init(wrappedValue:name:)
is the only initializer matching its declaration. On the other hand, the property height
does not provide an initial value so there are exactly two cases:
height
is using.init(name:)
, so a memberwise initializer without anheight
parameter should be generated;height
is using.init(wrappedValue:name:)
, so a memberwise initializer with anheight
parameter should be generated.
This has bad scalability implications, since now 2^n initializers would need to be generated, with n being the number of properties having no initial value and both .init(wrappedValue:...)
and .init(...)
available in their wrapper types.
Is this a reason why we cannot (or shouldn't) allow storage initialization in inits when wrappedValue
is not specified?
On the other hand, having those memberwise initializers generated that way, has some advantages:
- it hides the wrapper types, which is in line with the very first line of the Swift Programming Language Guide section about property wrappers:
A property wrapper adds a layer of separation between code that manages how a property is stored (the wrapper type and its implementation) and the code that defines a property.
- it makes your code more safe (kind of), since now you cannot pass an instance of
Field
with aname
different than the one specified in the annotation, i.e. the following wouldn't be possible:let person = Person(height: Field(name: "eye_color"))
I wonder if reducing the cases to be 1^n, i.e. by forcing one of the "overloads" or by using the "pick the most specific overload" already vastly used in the language, would be beneficial to the whole property wrapper field.