Qustion about property wrappers and init accessors

Using slightly modified example from the init accessor proposal

@propertyWrapper
struct Wrapper<T> {
  // changed from var to let
  let wrappedValue: T
}

struct S {
  @Wrapper private var value: Int

  init(value: Int) {
    self.value = value  // error: cannot assign to property: 'value' is a get-only property
  }
}

Why does the compiler emit error? The property wrapper still has initializer which init accessor supposed to use.

If you don't use the syntactic sugar inline, you have to use the initializer explicitly, or assign another wrapper.

@Wrapper private var value = 10

init(value: Int) {
  _value = .init(wrappedValue: value)

  @Wrapper var value = value
  self._value = _value
}
1 Like

Thank you for the illustration, but I felt dizzy for a second or two before it clicked. :slight_smile:

@Wrapper private var value = 10

init(value: Int) {
  _value = .init(wrappedValue: value)

  @Wrapper var anotherValue = value
  self._value = _anotherValue
}

If I understood you correctly, it contradicts with the proposal:

Property wrappers support bespoke definite initialization that allows initializing the backing property wrapper storage via the computed property, always re-writing initialization-via-wrapped-property in the form self.value = value to initialization of the backing storage in the form of _value = Wrapper(wrappedValue: value)

Also my example compiles if wrappedValue is mutable property.

For the time being this is how the feature was implemented. Depending on the effort you want to invest, you might want to consider writing an attached macro, that will create something like an immutable property wrapper for you AND supports the syntax on init, that you are looking for (I think).

This is a rough sketch on how this would work. Key here is the generation of an init accessor with storage restriction and to make use of a generated hidden wrapper value.

/// Defined in Macro library:
public struct Wrapper {
  public let value: String
}


/// Example:
class ExampleClass {
  // Developer writes:
  @Wrapper
  var value: String

  // Macro expands to:
  let _$valueWrapper: Wrapper // Generated by the attached macro (via 'peer' implementation).

  var value: String { // Generated by the attached macro (via `accessor` implementation).
    @storageRestrictions(initializes: _$valueWrapper)
    init(initialValue) {
      _$valueWrapper = Wrapper(value: initialValue)
    }
    get {
      _$valueWrapper.value
    }
  }

  // Now this will work:
  init() {
    value = "test"
  }
}

Thanks for advice. I'm fine with a mutable property. I thought it's a bug maybe.

That section of the proposal does not apply to immutable wrappedValues, but it's also just not consistent with how the language works otherwise and so I recommend against using it.

// Does not crash when initialized.
struct S {
  var value = 1 { didSet { fatalError() } }
  init(value: Int) {
    self.value = value
    self.value = value
    // Keep reassigning. `didSet` will never be called.
  }
}
@propertyWrapper struct Wrapper<T> {
  var wrappedValue: T
}

struct S {
  @Wrapper var value: Int { didSet { fatalError() } }
  init(value: Int) {
    self.value = value
    self.value = value // crash here, the first time `init` is not secretly called.
  }
}
@propertyWrapper struct Wrapper<T> {
  // Same behavior if `didSet` is moved here: 
  var wrappedValue: T { didSet { fatalError() } }
}

struct S {
  @Wrapper var value: Int
  init(value: Int) {
    self.value = value
    self.value = value // crash here, the first time `init` is not secretly called.
  }
}

Where is it implied in the proposal? Behaviour of multi-initialization can be understood, but is surprising indeed.

I would not argue that it is. But, we can at least acknowledge that property wrappers are not mentioned in the section on Init accessors for read-only properties. (There's a typo in this code, and reassignment crashes the compiler, but it works otherwise!)

1 Like