Help understanding property wrappers

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 an height parameter should be generated;
  • height is using .init(wrappedValue:name:), so a memberwise initializer with an height 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 a name 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.