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?

3 Likes

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 - [SR-11209] Conditional projected property · Issue #53608 · apple/swift · GitHub) 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.

4 Likes

Are there any updates as to extending property wrappers as per the Future Directions section of the pitch?

I just recently ran into two of these problems today. Firstly, I needed to be able to make the storage of a property wrapper public. Secondly, I really needed a solution for the referencing enclosing self problem. It is sort of ridiculous that Apple gets to use an undocumented not-officially-supported feature of property wrappers for their framework (the referencing to enclosing self done by @Published), but we can't really for ours.

1 Like

You can do that with the projection, or a computed variable, depending on if you want to make it public for all usages, or just in one place

Everyone gets to use undocumented features. I've seen someone sprinkle @inline(__always) all over their code.

If you want referencing enclosing self to be officially supported, there's a simple way to do that: write a proposal for that! You don't even need to write an implementation, because it exists already! You can fix your problem yourself :)

3 Likes

The reason why I wanted to be able to make the storage public was because I wanted to provide a binding through its projection property as is convention for SwiftUI. As I have it right know, I access the storage using the projection and have a binding property on the wrapper itself, which is not ideal per se.

Use of undocumented features, attributes, and keywords is a dangerous game to be playing since AFAIK, they are not officially supported and are subject to change as their behaviour is undefined and they could even be removal all together for that matter.

Also, if I have some time in the next little bit, I will look into the proposal. Thank you!

3 Likes

Hi Wildchild9,

Regarding 'referencing enclosing self' and the danger of using unsupported features:

One thing to have in mind is that this specific feature does not have any run-time dependencies, since property wrappers are basically just syntactic sugar for something that you could write yourself (admittedly using a great deal of boilerplate).
So this means that once it is compiled, it will not break on new OS'es even though the feature is changed or even removed entirely.
So the danger involves having to change some code in the future if the feature changes or is removed.

Secondly, since Combine is already relying on this feature (in some form or another), there is a very good chance that someone from Apple will eventually drive a proposal for adding the feature.

One thing I have heard mentioned (I think it was from @Joe_Groff ) is that any use cases for property wrappers referencing enclosing self, are very welcome, since they can help make a future proposal stronger.

4 Likes

I want to link this conversation from twitter: https://twitter.com/dgregor79/status/1184116757323120640

So some compiler devs like @Douglas_Gregor know about a different potentially more powerful design pitched by @jrose that we could tackle next. However so far nothing from that discussion was brought to the forums yet.

3 Likes

I think you just resolved your own complaint :stuck_out_tongue_winking_eye:

The work needed for something to work for one use case is not the same as what's required for every use case.

Regardless, I'm very much looking forward to addition PropertyWrapper features, so if you have time to work out a proposal, that'd be great!

4 Likes

I thought Jordan was going to post it, but instead I'll do it for him in a completely unstructured form (sorry):

class Speaker {
  // Once again, the problem with this in today's property wrappers
  // implementation is that every instance ends up storing the range, even
  // though by construction it can't ever be different from instance to
  // instance. That's a real waste. So what if we have a notion of "shared info
  // property wrappers" that live on the type and get instantiated?
  @Clamped(0...11) var volume: Int = 0
}

@sharedInfoPropertyWrapper
struct Clamped<Value: Comparable> {
  var initialValue: Value
  var range: ClosedRange<Value>

  // First off, the SharedInfo can have its own projectedValue property to
  // expose a static $foo property.
  var projectedValue: ClosedRange<Value> { range }

  // This initializer is the same as for today's property wrappers. However,
  // it's initializing the type-level wrapper, not the instance-level one. Like
  // current property wrappers, this can take an initial value or not.
  //
  // This makes a little more sense to me than my previous version purely
  // because of how the initializer is invoked at the use site.
  init(wrappedValue: Int, _ range: ClosedRange<Value>) {
    self.initialValue = range.clamping(wrappedValue)
    self.range = range
  }

  // The `instantiate()` function determines the type of the per-instance
  // storage, and is used by default to create instances. A client type can
  // always instantiate instances manually too in *its* initializer.
  func instantiate() -> Instance {
    return Instance(storage: sharedInfo.initialValue)
  }

  // This is what actually shows up as a stored property on the instance.
  struct Instance {
    var storage: Value

    // Use a subscript so that we have storage semantics but also have access
    // to the shared info.
    subscript(wrappedValueWithSharedInfo sharedInfo: Clamped<Value>) -> Value {
      get { storage }
      set { storage = sharedInfo.range.clamping(newValue) }
    }
  }
}

// One thing I'm not sure about is what to call the Clamped<Value> static
// property. The projection is probably `static var $volume: ClosedRange<Int>`,
// but having the shared info storage be `static var _volume: Clamped<Int>`
// seems a little weird. Nothing good comes to mind, however, especially when
// remembering that people can name their properties with non-English names. If
// we don't care about people accessing it directly we can do something like
// `static var volume$sharedInfo: Clamped<Int>` though.


// How does this scale to the (secret) mechanism for accessing the owner of a
// property? The best way is to provide the key paths that are currently in the
// (underscored) static subscript form of property wrappers directly to the
// initializer. Let's see that with "Observed".

protocol Observable: AnyObject {
  func notifyObserversAsync<Value>(for keyPath: KeyPath<Self, Value>)
}

@sharedInfoPropertyWrapper
struct Observed<Owner: Observable, Value> {
  var initialValue: Value
  var storageKeyPath: ReferenceWritableKeyPath<Owner, Instance>
  var wrappedValueKeyPath: ReferenceWritableKeyPath<Owner, Value>

  // We use a new kind of default argument here instead of magic names.
  // This allows regular arguments and property-wrapper-related implicit
  // arguments to coexist better than in the current scheme. Other magic 
  // arguments could include #propertyWrapperProjectionKeyPath and
  // #propertyWrapperOwnerType.
  init(
    wrappedValue: Value = #propertyWrapperInitialValue,
    storageKeyPath: ReferenceWritableKeyPath<Owner, Instance> = #propertyWrapper
StorageKeyPath
    wrappedValueKeyPath: ReferenceWritableKeyPath<Owner, Value> = #propertyWrapp
erValueKeyPath
  ) {
    self.initialValue = wrappedValue
    self.storageKeyPath = storageKeyPath
    self.wrappedValueKeyPath = wrappedValueKeyPath
  }

  func instantiate() -> Instance { return .init(storage: initialValue) }

  struct Instance {
    var storage: Value

    // This replaces the static subscript we have today. (It could live on
    // either the Instance or the outer wrapper type, since it doesn't have any
    // context of its own. I put it here for consistency with the non-static
    // one, but the outer one would get to use 'Self' in the signature, which is
    // a little nice.)
    //
    // One downside of this approach is that it's a little less easy to support
    // working with both mutable and immutable properties. With today's static
    // subscript, you can overload on the key path kind; in the shared-info
    // version, you'd have to store either a KeyPath or a
    // ReferenceWritableKeyPath and keep track. Or just store a KeyPath and try
    // downcasting (ick). And it still doesn't give you access to a struct
    // value.
    static subscript(
      _enclosingInstance instance: Owner,
      sharedInfo: Observed<Owner, Value>
    ) -> Value {
      get { instance[keyPath: sharedInfo.storageKeyPath].storage }
      set {
        instance[keyPath: sharedInfo.storageKeyPath].storage = newValue
        instance.notifyObserversAsync(for: sharedInfo.wrappedValueKeyPath)
      }
    }
  }
}

// We should also have equivalents of both subscripts for the projectedValue.

// P.S. What happens if you use a property wrapper with shared info on a
// non-instance property? Do you get an error, or does it just put the shared
// info alongside the regular property? In the latter case, what happens with
// the projected value?

// To close out, here are two kinds of Lazy we can do: one that's just like the
// example Doug showed at WWDC but more efficient...

@sharedInfoPropertyWrapper
struct Lazy<Value> {
  var compute: () -> Value

  init(wrappedValue: @autoclosure () -> Value) {
    self.compute = wrappedValue
  }

  func instantiate() -> Instance { return .init() }

  struct Instance {
    var value: Value? = nil

    subscript(wrappedValueWithSharedInfo sharedInfo: Lazy<Value>) -> Value {
      mutating get {
        if let value = self.value {
          return value
        }
        value = sharedInfo.compute()
        return value!
      }
      set {
        value = newValue
      }
    }
  }
}

class Calculator {
  @Lazy var regionalTipRates = Dictionary(contentsOf: …)
}

// ...and one that allows referencing 'self', sort of, at the cost of some
// goofy-looking syntax at the use site.

@sharedInfoPropertyWrapper
struct LazyFromSelf<Owner: AnyObject, Value> {
  var compute: (Owner) -> Value
  var storageKeyPath: ReferenceWritableKeyPath<Owner, Instance>

  init(
    _ compute: (Owner) -> Value,
    storageKeyPath: ReferenceWritableKeyPath<Owner, Instance> = #propertyWrapperStorageKeyPath
  ) {
    self.compute = compute
    self.storageKeyPath = storageKeyPath
  }

  func instantiate() -> Instance { return .init() }

  struct Instance {
    var value: Value? = nil

    static subscript(
      instance: Owner,
      sharedInfo: LazyFromSelf<Owner, Value>
    ) -> Value {
      get {
        if let value = instance[keyPath: sharedInfo.storageKeyPath].value {
          return value
        }
        let result = sharedInfo.compute(instance)
        instance[keyPath: sharedInfo.storageKeyPath].value = result
        return result
      }
      set {
        instance[keyPath: sharedInfo.storageKeyPath].value = newValue
      }
    }
  }
}

class WindowController {
  // Yeah, ick. We'd need some *additional* feature to make this better. I think
  // I've gone on long enough, though.
  @LazyFromSelf({ (self_: Self) in
    self_.loadWindow()
  }) var window: Window
}
10 Likes

Couldn't the same (and more in fields besides property wrappers) be achieved with an extension of generics that has been discussed long ago?
Afair there has never been an agreement how to spell parameters that aren't types, but it could be like
struct Clamped<Value: Comparable, let min: Value, let max: Value>.

4 Likes

The feature you mention would certainly address the @Clamped use case where we are working with constant values. However, it doesn't address the notion of having "per declared property" storage. I suspect there is significant overlap in the use cases of the two approaches, but that we can find cases where one or the other is better. This area could use more exploration for sure.

Doug

2 Likes

Thanks for posting the ideas. I have hard times understanding them all, but I start to get a feeling where this goes. I think there is still a lot room for improvement here.

There are two scenarios where you may want to share a property wrapper at the type level.

  1. One is presented in your examples and it‘s about sharing a single property wrapper for a specific property between multiple instances.
  2. The other one is when you want to share a single property wrapper between multiple properties on a single instance.I think it‘s even more important because it might also affect local wrapped variables.

To begin with I think a better name for the attribute should be just an extension around the current one instead of a brand new attribute: @propertyWrapper(shared)
That would signal different semantics to the user.

Personally I tend to say that (2) is more important than (1), but the problems that (1) solves shouldn‘t be ignored. The idea of the Wrapper.Instance type is quite interesting and if I‘m not mistaken, we could extend it so we can potentially combine (1) and (2) together to cover all of the issues with a single design.

In (2) a property wrapper is basically a proxy type that processes of the behavior we want. A shared property wrapper should only exist once per type or once in general (that‘s debatable). To be able to merge (1) into (2) we need a property specific storage that is shared statically across instances. To do that we can build on top of Wrapper.Instance idea and create some Wrapper.Context which will be a static property.

To sum up, a more advanced property wrapper may consist of 3 parts:

  • Wrapper the behavioral proxy type that requires a shared context
  • (optional) Wrapper.Context shared statically between instances and therefore is always bind to a specify instance property
  • Wrapper.Instance the storage that lives on the instance itself which is not shared

I tagged Context as optional on purpose, because I strongly feel that this functionally is not always needed and therefore should not be required if the user wants only to achieve the semantics of (2) where the instance stores some data and reuses the same single wrapper.

PS: I‘ll try to write some pseudocode examples soon.


Alternative names for Context and Instance could be PropertyContext and PropertyStorage which signal their intend better.

@Wrapper var foo: Value

// potentially desuggars to 

// should this be mutable since we share it across instances
private static let _foo: Wrapper.PropertyContext = ...
private var _foo: Wrapper.PropertyStorage = ...
var foo: Value {
  get {
    Wrapper.shared[context: Swlf._foo, instance: self].wrappedValue
  }
  set {
    Wrapper.shared[context: Swlf._foo, instance: self].wrappedValue = newValue
  }
}

Value types may require some alternative parameter, potentially inout or something else.

Here is one full example, which still combines some old design ideas with previously suggested ways of splitting up storage.

@propertyWrapper(shared)
public struct Clamped<Value> where Value: Comparable {
  public struct Context {
    public let range: ClosedRange<Value>
    public init(range: ClosedRange<Value>) {
      self.range = range
    }
  }

  public struct Storage {
    var value: Value
    public init(wrappedValue: Value) {
      self.value = wrappedValue
    }
  }

  private static func _clamp(
    value: Value, 
    using range: ClosedRange<Value>
  ) -> Value {
    range.clamping(value)
  }

  public let context: Context
  private var _storage: Storage

  // For locally wrapped properties 
  public var wrappedValue: Value {
    get {
      _storage.value
    }
    set {
      _storage.value = Self._clamp(value: newValue, using: context.range) 
    }
  }

  public var projectedValue: ClosedRange<Value> {
    context.range
  }

  // For properties on value types:
  // (function name == `accessor + wrappedValue` or `accessor + projectedValue`)
  public static func getWrappedValue(
    context: Context, 
    storage: Storage
  ) -> Value {
    storage.value
  }

  public static func setWrappedValue(
    newValue: Value 
    context: Context,
    storage: inout Storage
  ) {
    storage.value = _clamp(value: newValue, using: range)
  }

  public static func getProjectedValue(
    context: Context, 
    storage: Storage
  ) -> Value {
    storage.value
  }

  public init(wrappedValue: Value, range: ClosedRange<Value>) {
    self.context = Context(range: range)
    self._storage = Storage(wrappedValue: wrappedValue)
  }

  // For reference types
  public static subscript <EnclosingSelf>(
    context: Context,
    enclosingSelf: EnclosingSelf,
    storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Storage>,
    wrappedValueKeyPath: KeyPath<EnclosingSelf, Value>
  ) -> Value where EnclosingSelf: AnyObject {
    get {
      enclosingSelf[keyPath: storageKeyPath].value
    }
    set {
      enclosingSelf[keyPath: storageKeyPath].value = _clamp(value: newValue, using: context.range)
    }
  }

  public static subscript <EnclosingSelf>(
    context: Context,
    enclosingSelf: EnclosingSelf,
    storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Storage>,
    projectedValueKeyPath: KeyPath<EnclosingSelf, ClosedRange<Value>>
  ) -> ClosedRange<Value> {
    context.range
  }
}

// Value type context
struct S {
  @Clamped(0 ... 11) 
  var volume: Int = 0

  // desuggars to
  private static let _volume = Clamped<Int>.Context(range: 0 ... 11)
  private var _volume = Clamped<Int>.Storage(wrappedValue: 0)
  var volume: Int {
    get {
      Clamped<Int>.getWrappedValue(context: Self._volume, storage: _volume)
    }
    set {
      Clamped<Int>.setWrappedValue(newValue: newValue, context: Self._volume, storage: &_volume)
    }
  }
  var $volume: ClosedRange {
    Clamped<Int>.getProjectedValue(context: Self._volume, storage: _volume)
  }
}

// Object context
class C {
  @Clamped(0 ... 11) 
  var volume: Int = 0

  // desuggars to
  private static let _volume = Clamped<Int>.Context(range: 0 ... 11)
  private var _volume = Clamped<Int>.Storage(wrappedValue: 0)
  var volume: Int {
    get {
      Clamped<Int>[
        context: Self._volume, 
        enclosingSelf: self, 
        storageKeyPath: \._volume,
        wrappedValueKeyPath: \.volume  
      ]
    }
    set {
      Clamped<Int>[
        context: Self._volume, 
        enclosingSelf: self, 
        storageKeyPath: \._volume,
        wrappedValueKeyPath: \.volume  
      ] = newValue
    }
  }
  var $volume: ClosedRange<Int> {
     Clamped<Int>[
       context: Self._volume, 
       enclosingSelf: self, 
       storageKeyPath: \._volume,
       projectedValueKeyPath: \.$volume,
     ]
  }
}

// Local property context
func foo() {
  @Clamped(0 ... 11) 
  var volume: Int = 0

  // desuggars to
  var _volume = Clamped<Int>(wrappedValue: 0, range: 0 ... 11) // invisible
  var volume: Int {
    get {
      _volume.wrappedValue
    }
    set {
      _volume.wrappedValue = newValue
    }
  }
  var $volume: ClosedRange<Value> {
    _volume.projectedValue
  }
}

In this case we don't even need to call the attribute @propertyWrapper(shared) because the type can start simple like we do it today already. If we want to provide an optimization or an ability to inject self into the wrapper, we can add special types and static methods/subscripts.

Optimization for properties on value types:

  • (required) Context type
  • (required) Storage type
  • getWrappedValue, setWrappedValue
  • getProjectedValue, setProjectedValue

Optimization for properties on reference types:

  • (optional) Context type
  • (required) Storage type
  • subscript(enclosingSelf:storageKeyPath:wrappedValueKeyPath:) or subscript(context:enclosingSelf:storageKeyPath:wrappedValueKeyPath:)
  • subscript(enclosingSelf:storageKeyPath:projectedValueKeyPath:) or subscript(context:enclosingSelf:storageKeyPath:projectedValueKeyPath:)

The amount of boilerplate we'd need for these optimizations is quite large, and the rules are not simple to explain to a beginner, but this is an optimization that allows us to solve most if not all remaining use cases.

If we can find a better simplification for this, even better.

Maybe we can get the optimization for value types as well, which would depend on two subscript related features mentioned here:

Hello folks!

Regarding this future direction with enclosing self access, I have a question:

Why was it that the suggested (and currently privately implemented) API takes a wrappedValueKeyPath as parameter, and still have a concept of wrappedValue property to begin with, while:

  1. the value of this wrapped value will already be provided by the static subscript in this case rather than by the wrappedValue property
  2. even if we needed it, this KeyPath could already be constructed by storageKeyPath.appending(path: \.wrappedValue) anyway

For example, in this gist by @Ian_Keen we can see that wrappedValue is unused – but since it has to be implemented it returns fatalError()

imho that introduces an unnecessary requirement (unused wrappedValue property) and also cognitive overhead having 2 different KeyPaths (which can be easily confused) in the subscript parameters


I propose that we get rid of the restriction of requiring a wrappedValue property if we have the static subscript. In fact, it probably makes sense that we'd need the requirement to be either one or the other (either have a wrappedValue property, or have the static subscript)

This also means that we could get rid of that KeyPath in the static subscript parameter list. And take the occasion to rename storageKeyPath (which seems a bit obscure to me, why name it "storage" while in every other place we refer to this as "the wrapper" not "the storage"?) to be wrapperKeyPath instead

static subscript<Instance>(
    enclosingInstance instance: Instance,
    wrapperKeyPath: ReferenceWritableKeyPath<Instance, Self>
) -> Value {
    // implementation example for the `Derived` wrapped example posted above
    get { instance[keyPath: wrapperKeyPath].getter(instance) }
    set { instance[keyPath: wrapperKeyPath].setter(instance, newValue) }
}

Thoughts?

4 Likes

The labels and the parameter names aren't perfectly aligned with the current design of property wrappers that is true. However wrapped wrappedKeyPath is still needed for a simple reason. You have to be able to disambiguate between the static subscript which returns the wrapped value vs. the static subscript that returns the projected value instead.

I had some short conversations on Twitter with @Joe_Groff, @Douglas_Gregor and @jrose and I no longer think that the privately available feature should ever become available as the final way on how we would access self.

However the ideal solution will only be available in the long term as it would require generalized coroutines.

@propertyWrapper(static)
struct W<Value> {
  static func read<T>(from instance: T) -> shared Value { ... }
  static func modify<T>(in instance: inout T) -> inout Value { ... }
  // combined with some of the upthread ideas
}

Something like this would allow us multiple things:

  • We could share constant values between static wrappers directly through the types that contain wrapped properties
  • This would reduce the type of the types
  • This would be an optimization for both value an reference types
  • Types would contain only the necessary storages, not the property wrappers as they will be static
  • The solution would be more generalized and more elegant
  • It would obviously allow access to enclosing self
  • It wouldn't contain the clattered static subscript syntax anymore
11 Likes

As a language user, I am struggling to see a distinction within established rules between the said "static wrapper" design and the current static subscript implementation. I am assuming that the goal is to have a design enabling efficient in-place access, and hence the emphasis on coroutines.

So may I ask if static subscripts with corotuine accessors were ever on the table? As far as I can understand, it doesn't seem to go against the ownership model, and inout reference of a key path access seems optimizible if the key path refers to a directly-accessible, stored property.

For example:

@propertyWrapper
public struct Diffable<Collection: BidirectionalCollection>
    where Collection.Element: Equatable
{
    let value: Collection

    public static subscript<OuterSelf: Aggregator>(
        instanceSelf container: inout OuterSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Collection>,
        storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Collection>
    ) -> Collection {
        _read { yield &container[keyPath: storage].value }
        _modify {
            let old = container[keyPath: storage].value
            yield &container[keyPath: storage].value
            let diff = container[keyPath: storage].value.difference(from: old)
            container.subcollectionDidChange(at: keyPath, difference: diff)
        }
    }
}
  • We could share constant values between static wrappers directly through the types that contain wrapped properties

It would require the "enclosing self" to be constrained to some generic contexts to have a meaningful access to constants. It appears that static subscripts can achieve this already, isn't it?

  • This would be an optimization for both value an reference types

I am not sure what optimization means in the context here.

If it refers to having unified coroutine "accessors" for value types and reference types, it cannot be true. A separate modify path with a <T: AnyObject>(_: T) signature is required to work with reference types, since inout T would otherwise allow enclosing self replacement.

  • Types would contain only the necessary storages, not the property wrappers as they will be static

This appears to describe the storage synthesis to be based on some unspecified form of declarations. What would be the benefit/distinction over a property wrapper? After all, a property wrapper in general is a product of all the necessary storages.

  • This would reduce the type of the types
  • The solution would be more generalized and more elegant
  • It would obviously allow access to enclosing self
  • It wouldn't contain the clattered static subscript syntax anymore

These seem to be a mix of personal opinions and unelaborated benefits (semantically or implementation-wise).

1 Like

Thank you for your feedback. My personal view of the future of PW's is inspired by the upthread ideas from @jrose which where posted by @Douglas_Gregor with some sugar provided by @Joe_Groff somewhere on twitter. Maybe they can join the conversation and share their thoughts about where this feature should really go. Last year the design process was a little bit hectic. Today I think if we had more time back then and if some of the mentioned missing language feature existed then I could imagine a single design possibly around static types, which would cover almost every possible use-case.

IMHO, subscripts overloading is important to have. The compiler error:

Property wrapper type '***' has multiple enclosing-self subscripts 'subscript(_enclosingInstance:wrapped:storage:)'

becomes a roadblock more than one times when playing with PWs.

1 Like