Allow @propertyWrapper inside extensions

Some property wrappers store their wrappedValues inside some external storage (like UserDefaults, View Environment etc.) rather than storing it in-place. So if for convenience' sake we separate property wrappers into two types where first type would be like this:

@propertyWrapper
struct FirstWrapper {
    var wrappedValue: Int
}

and second type would be like this:

@propertyWrapper
struct SecondWrapper {
    var wrappedValue: Int {
        get {
            defaults.integer(forKey: key)
        }
        nonmutating set {
            defaults.set(newValue, forKey: key)
        }
    }
    
    let defaults: UserDefaults
    let key: String
}
// or
@propertyWrapper
struct SecondWrapper {
    var wrappedValue: Int {
        fatalError("Unimplemented")
    }
    
    let defaults: UserDefaults
    let key: String
    
    static subscript<T>(
        _enclosingInstance instance: T,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Int>,
        storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
    ) -> Int {
        get {
            let wrapper = instance[keyPath: storageKeyPath]
            return wrapper.defaults.integer(forKey: wrapper.key)
        }
        set {
            let wrapper = instance[keyPath: storageKeyPath]
            wrapper.defaults.set(newValue, forKey: wrapper.key)
        }
    }
}

I propose to allow declaring propertyWrappers of second type in extensions like so:

extension Foo {
    @SecondWrapper(defaults: .standard, key: "bar")
    var bar: Int
}

which would be then desugarized to

extension Foo {
    var _bar: SecondWrapper {
        SecondWrapper(defaults: .standard, key: "bar")
    }
    
    var bar: Int {
        get {
            _bar.wrappedValue
        }
        set {
            _bar.wrappedValue = newValue
        }
    }
    // or
    var bar: Int {
        get {
            _bar[self, wrapped: \.bar, storage: \._bar]
        }
        set {
            _bar[self, wrapped: \.bar, storage: \._bar] = newValue
        }
    }
}

This can't happen for the same reason ordinary stored properties can't be added in extensions: your SecondWrapper declares two stored properties, defaults and key, which will ultimately be required to land in the enclosing type Foo, and you can't add storage in extensions. This restriction has already been listed in the original proposal.

Yes, SecondWrapper does have stored properties, but _bar is computed property, thus it can happen.

Ok, I see. So how would the compiler need to decide whether it is allowed to recompute the wrapper each time (i.e., what's the formal difference between the first type and the second type)? By looking at the fact that all properties in the wrapper are immutable?

This also raises a forwards compatibility issue: what happens if the implementation of the wrapper changes and it decides to require some mutable backing storage? Every extension having this wrapper will suddenly become ill-formed, even though it is not a public-facing change.

1 Like

There is a future direction in SE-0258 which may allow wrapped properties in extensions:

Your example would then become something like the following:

extension Foo {
  var _bar: SecondWrapper {
    get { .init(defaults: .standard, key: "bar") }
    set { /* do nothing */ }
  }

  @wrapper(to: _bar) var bar: Int
}

I think declaring property-wrapped properties in extensions would be better tackled by a combination of 'stateless' wrappers and access to enclosing self:

@propertyWrapper(computed) // Perhaps 'stateless'?
struct EnvironmentValue<Key : EnvironmentKey> {

  // Constants are okay.
  let key: Key.Type

  // Stateless version of the subscript  enclosing 'self' access.
  subscript(
    enclosingInstance: EnvironmentValues, 
    wrapped wrappedKeyPath: KeyPath<EnvironmentValues, Key.Value>,
    storage storageKeyPath: KeyPath<EnvironmentValues, Self>
  ) -> Key.Value {
    ...
  }

}


extension EnvironmentValues {

  @EnvironmentValue(key: MyColorPropertyKey.self) 
  var myColorProperty: Color

}

This approach would also alleviate the need for a special feature that delegates its storage to an existing property:

@propertyWrapper(computed)
struct DelegatedProperty<Value> { ... }

struct Color {

  private var _components: (red: Double, green: Double, blue: Double)

  
  @DelegatedProperty(\._components.red)
  var red: Double

}

To go into a bit more detail:

  1. Wrappers would need to be explicitly declared stateless (maybe access to enclosing self should also be explicitly stated).
  2. Such wrappers can only contain constant properties.

Obviously, there are many aspects of the design that I haven't considered; nevertheless, I think it's a good starting point.

What to you think?

I think minimal requirements for property wrappers to be used in extensions is to be struct with nonmutating setters both in wrappedValue and enclosingInstance subscript

Agreed. Probably filip-sakel's suggestion with 'stateless' wrappers fits there.

Cool enough for me

A more nit-picky concern, but still: the proposed wrappers are being instantiated on each access, while the "normal" wrappers do so only once. As such, they have very different performance or side effects implications, while looking the same at their usage point. Won't this be a confusion point?

@filip-sakel A backward question (somewhat related to my concern above): what happens to a computed/stateless wrapper when used in the base declaration of a type (i.e., not in an extension)? Should it stay computed (it probably should for consistency sake) — but bear those performance implications — or act as a "normal" one and allocate its required storage etc., but behaving very differently from when it would've been used in an extension?

I think these constraints would suffice –– at least for the beginning.

In my opinion, this depends on the quality of documentation of such wrappers.

I think stateless wrappers should always be computed.

I'm not sure what performance implications you referring to. I'm not very familiar with compiler architecture, but I think inlining and other optimizations should ensure minimal, if any, performance penalties.

Imagine a computed property having an heavy to compute getter. Every time you read that property, you have to recompute its value. If the property wasn't computed, you would only compute it the first time, storing the resulting value in the type property.


I'm having trouble understanding your @propertyWrapper(computed) examples, could you add the omitted bits?

  • The subscript(enclosingInstance:wrapped:storage:) isn't static, is it intended?
  • How would the generated getter/setter be (or only getter in this case)? If the subscript is not static, you'll need a wrapper instance.

I'm sorry for the confusion; trying to retain compatibility with the current design I forgot that such wrappers wouldn't have backing storage; they should be eagerly computed on each get and set. Taking that into account, @EnvironmentKey should be declared as such:

struct EnvironmentValue<Key : EnvironmentKey> {

  // Constants are okay.
  let key: Key.Type

  // Takes in 'self', 'enclosingInstance' and 'wrapped'; 
  // returns value of type 'Value'.
  subscript(
    enclosingInstance: EnvironmentValues, 
    wrapped wrappedKeyPath: KeyPath<EnvironmentValues, Key.Value>
  ) -> Key.Value {
    get {
      enclosingInstance[key] 
    }
    set {
      // EnvironmentValues actually have a mutating setter, but
      // I don't won't to get into the discussion of enclosing-self syntax 
      enclosingInstance[key] = newValue
    }
  }

}

Is my enclosing-self, stateless-wrapper syntax clearer now?

What you describe sounds to me like lazy semantics, for which stateless wrappers should not, in my opinion, be used. Perhaps I didn't make that clear, but I view stateless wrappers as simple, lightweight wrappers –– probably without side-effects –– that should, in theory, be inlinable.

In my color example, @DelegatedProperty should ideally be equivalent to manually writing a computed property that gets and sets the corresponding color component (in this case, red). As for side-effects, this wrapper would act as a function that takes EnclosingInstance and Self and returns Value for its getter; likewise, the setter would take inout EnclosingInstance and Self. No side-effects occur in the wrapper's getters and setters accesses.

struct Color {

  private var _components: (red: Double, green: Double, blue: Double)

  
  @DelegatedProperty(\._components.red)
  var red: Double

  // === Becomes --------------------

  var red: Double {
    get {
      DelegatedProperty(\._components.red)[
        self, 
        wrapped: \Color.red
      ]
    }
    set {
      DelegatedProperty(\._components.red)[
        &self, 
        wrapped: \Color.red
      ] = newValue
    }
  }   

  // === Similar to -------------------

   var red: Double {
    get {
      _components.red
    }
    set {
      _components.red = newValue
    }
  }   

}

Again, syntax for access of the enclosing-self has not been fully fledged-out meaning that @DelegatedProperty would not be valid (it would also have be declared with generic parameters: Root and Value.

The current limitations are not debilitating or unsolvable. In fact, if I recall correctly, it has been proposed that, instead of subscripts, enclosing-self access should use read and write methods, which would solve @EnvironmentValue's problem.

In regards to determining Root for @DelegatedProperty, I think we could make the compiler recognize the "enclosingInstanceType" parameter name in the initializer which, when bound to a generic parameter of the property-wrapper type, would help type inference understand that a given generic parameter should be inferred to be the enclosing-self type. Thus, when writing @DelegatedProperty(\._components) var red the compiler would infer that we mean \Color.red. If this approach for inferring generic parameters to be the enclosing-self type proves to be too cumbersome, an attribute would be another option worth considering.

Terms of Service

Privacy Policy

Cookie Policy