Future directions of Property Wrappers

In SE-0258 there is mentioned a future direction of the feature - namely Referencing the enclosing 'self' in a wrapper type.

This future direction is already implemented in a preview version in the Swift toolchain that is part of the latest Xcode 11 betas.

This feature is already used in Combine as part of the mechanics used to synthesize the objectWillChange of an ObservableObject when @Published is used.

Currently I cannot link to the documentation for ObservableObject on Apple's developer site (can't find it anymore) but the synthesis was at least previously mentioned in the documentation and later elaborated on by @Douglas_Gregor in this thread on Twitter.

The implementation of the current state of the feature can be found here.

@Douglas_Gregor later mentioned that other use cases for the above mentioned 'future directions' was welcomed since the only one currently mentioned is the exact use case for the @Published property wrapper.

I've only played very little with the hidden feature (it was a bit hard to dig out even with the hints on Twitter :wink:), but after a recent twitter chat with @Joe_Groff, I realized that this feature will actually enable a use case that I have wished for since I first started playing with property wrappers:

Consider a NoSQL database like the Firebase Real-time Database. Entities in this database can be considered to reside at a path of keys in a key-value tree. An example in the Firebase documentation is the model of a collection of chatrooms. Each chat room could have a list of messages and perhaps some configuration. A model of this in a key-value store could look like:

{
  chatrooms: {
    firechat: {
      configuration: {
        name: "FireChat"
      },
      messages: { ... }
    },
    swiftchat: {
      configuration: {
        name: "Swift chat"
      },
      messages: { ... }
    },
    ... other chat rooms ...
  }
}

Here the keys firechat and swiftchat are identifiers of individual chat rooms that each have a similar structure, namely some configuration entity and some collection of messages.

Now consider a property wrapper that could be initialized with the path to an entity and then represent this entity as a model value that is kept in sync with the database. Let's call this wrapper FirebaseValue. This could be used with the above example as:

@FirebaseValue(path: "/chatrooms/firechat/configuration")
var configuration: Configuration

Where Configuration might be implemented as:

struct Configuration: Codable {
  var name: String
}

This is all really great, but now consider if you wish to use a Configuration from some view model that was parametrized with the identifier of a chat room:

class ViewModel {
   let chatroomId: String
   init(chatroomId: String) {
     self.chatroomId = chatroomId
   }
   @FirebaseValue(path: "/chatrooms/\(self.chatroomId)/configuration")
   var configuration: Configuration
}

This unfortunately can't work since the initializer of the property wrapper cannot be called lazily.
In the future we may be able to use property wrappers in local scope to do something like:

class ViewModel {
   init(chatroomId: String) {
     self._configuration = @FirebaseValue(path: "/chatrooms/\(chatroomId)/configuration") var configuration: Configuration
   }
   @FirebaseValue
   var configuration: Configuration
}

I'm not 100% sure about the notation for this, but I don't think that it is very easy to read with the almost dual definition of the configuration variable. I'd much rather think of the path as part of the definition of the property like in the first example, but I'd also really, really like the dynamic aspect of being able to refer to another property as part of the initializer.

And now finally to the link to the Referencing the enclosing 'self' feature:

This feature can be used along with keypaths to properties on Self (or alternatively closures from Self to a value) in order to lazily initialize actual values from the keypaths on the first access to the property (since here we are passed the _enclosingInstance and can use this to convert our keypaths to values.

A very simple proof-of-concept implementation of this (that only resolves a single keypath) is:

@propertyWrapper
struct ResolveKeypathLazily<Value, EnclosingSelf> {
    private var stored: Value
    private let aKeyPath: KeyPath<EnclosingSelf, String>
    private var resolved: String?

    public init(wrappedValue: Value, keyPath: KeyPath<EnclosingSelf, String>) {
        self.stored = wrappedValue
        self.aKeyPath = keyPath
    }

    var wrappedValue: Value {
        get { fatalError() }
        set { fatalError() }
    }

    public static subscript(
        _enclosingInstance observed: EnclosingSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
      ) -> Value {
      get {
        // Resolve the keypath
        if observed[keyPath: storageKeyPath].resolved == nil {
            let keyPath = observed[keyPath: storageKeyPath].aKeyPath
            observed[keyPath: storageKeyPath].resolved = observed[keyPath: keyPath]
        }

        return observed[keyPath: storageKeyPath].stored
      }
      set {
        // TODO: Also perform the resolution here.
        observed[keyPath: storageKeyPath].stored = newValue
      }
    }
}

The PoC does not actually use the resolved value for anything - it's just shown as a demonstration that it can be done.

This can be used as follows:

class TestResolver {
    var identifier: String

    @ResolveKeypathLazily(keyPath: \TestResolver.identifier)
    var hamster: Int = 1

    init(identifier: String) {
        self.identifier = identifier
    }
}

This would look a bit nicer if the implementation of the static subscript using a generic parameter of the type would cause the compiler to be able to infer this generic parameter. @Joe_Groff mentioned on twitter that the final version of the feature might be able to drop a hint to the compiler about inferring the generic parameter in case this type is used in the static subscript.

If that were the case, the example could look like:

class TestResolver {
    var identifier: String

    @ResolveKeypathLazily(keyPath: \.identifier)
    var hamster: Int = 1

    init(identifier: String) {
        self.identifier = identifier
    }
}

In general this gives a mechanism to lazily resolve values on the enclosing self by using keypaths in the initializer of the property wrapper.

The FirebaseValue example could then look like (with clever use of string interpolation):

class ViewModel {
   let chatroomId: String
   init(chatroomId: String) {
     self.chatroomId = chatroomId
   }
   @FirebaseValue(path: "/chatrooms/\(\.chatroomId)/configuration")
   var configuration: Configuration
}

There is however one caveat:
The lazy values can currently only be resolved once the 'wrappedValue' is accessed.
In case the projection is accessed first, then the values won't be resolved at this time.

So in order to make it possible to use keypaths (or closures) to model lazy values in the initializer for property wrappers, I propose adding a static subscript similar to the one for the wrappedValue with enclosingInstance, but for the projection instead.

Does anyone feel like this is a useful use case, and that it migth be a useful feature to add to the excellent property wrapper feature?

1 Like

Hi Adrian,
I am not exactly certain what you are editing - is it the use cases in the future directions part of the proposal?
Sincerely,
/morten

Sorry I forgot to reply. After property wrappers were finally released I immediately started using them. However, because Swift 5.1 is not out yet I use property wrappers in Swift 5 which implies that I have to write all the synthesized boilerplate code by hand.

I have found one more use-case that wasn't discussed in any of the property wrapper threads (conditional projected values - https://bugs.swift.org/browse/SR-11209) and filed a bug report for that, but @Douglas_Gregor has not responded to that issue yet.

I also needed a way to inject some outer self into the wrapper and gathered some ideas on how the static wrapper should be designed.

To solve your issue with projected values, I suggest that the static subscript should require specific label names.

@propertyWrapper
struct Wrapper<Value> {

  static subscript <EnclosingSelf>(
    enclosingSelf enclosingSelf: /* inout */ EnclosingSelf,
    wrappedValue wrappedValueKeyPath: /* Reference / Writable */ KeyPath<EnclosingSelf, Value>,
    wrapper wrapperKeyPath: /* Reference / Writable */ KeyPath<EnclosingSelf, Wrapper>
  ) -> Value /* where EnclosingSelf: SomeProtocol */ {
    ...
  }

  static subscript <EnclosingSelf>(
    enclosingSelf enclosingSelf: /* inout */ EnclosingSelf,
    projectedValue projectedValueKeyPath: /* Reference / Writable */ KeyPath<EnclosingSelf, OtherValue>,
    wrapper wrapperKeyPath: /* Reference / Writable */ KeyPath<EnclosingSelf, Wrapper>
  ) -> OtherValue /* where EnclosingSelf: SomeProtocol */ {
    ...
  }
}

In short the hidden feature is cool, but it's too restrictive and I would rather invite the community for another pitch on how to generalize it.

I think "theoretically" todays synthesizing could also use a static subscript even though it would mean a little more boilerplate, but it would keep the design the same.

Small example:

@propertyWrapper
struct W<V> {
  var wrappedValue: V
}

@W var property: Int = 42

// this is the same as
private var _property = W(wrappedValue: 42)
var property: Int { 
  get {
    return _property.wrappedValue
  }
  set {
    _property.wrappedValue = newValue
  }
}

Why can't we have this instead?

@propertyWrapper
struct W<V> {
  private var _value: V
 
  init(wrappedValue: V) {
    _value = wrappedValue
  }

  static subscript <EnclosingSelf>(
    enclosingSelf enclosingSelf: inout EnclosingSelf,
    wrappedValue wrappedValueKeyPath: WritableKeyPath<EnclosingSelf, Value>,
    wrapper wrapperKeyPath: WritableKeyPath<EnclosingSelf, W>
  ) -> Value  {
    get {
      return enclosingSelf[keyPath: wrapperKeyPath]._value
    }
    set {
      enclosingSelf[keyPath: wrapperKeyPath]._value = newValue
    }
  }
}

struct T {
  @W var property: Int = 42
  
  // this is the same as
  private var _property = W(wrappedValue: 42)
  var property: Int { 
    get {
      // issue this requires `get` to be `mutating`
      return W[enclosingSelf: &self, wrappedValue: \.property, wrapper: \._property]
    }
    set {
      W[enclosingSelf: &self, wrappedValue: \.property, wrapper: \._property] = newValue
    }
  }
}

However subscript today do not allow inout parameters. My assumption is that if we had read instead of get then we could maybe use yield to workaround the mutating get limitation, but I also could be wrong.

Other than that I think the requirement of a fixed set of subscript labels should resolve most issues. What I really like about that design is that we can have multiple overloads of the subscript for different EnclosingSelf or even Value constraints.


wrappedValueKeyPath should not require ReferenceWritableKeyPath because the subscript itself might be get only which then implies that the wrapped property will be get only and allow only a simple KeyPath.

1 Like
Terms of Service

Privacy Policy

Cookie Policy