SE-0258: Property Delegates

What is your evaluation of the proposal?

50/50

Is the problem being addressed significant enough to warrant a change to Swift?

If it is decided to never have atomic in Swift, I’m afraid we have no choice. So, yes.

Does this proposal fit well with the feel and direction of Swift?

Not sure, the new syntax i$ very confusing.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

N/A

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I read through both pitches.

+1
I read the proposal and have followed the various threads that have lead to this.

I agree that the syntax is heavier than most of the features that we have seen thus far but I don't know that the proposed solutions are much better or are workable.

I regret that delegates aren't protocols and can't be combined.

Even in a simple use case I've needed combinations of delegates: I coded an app where the user could read and edit documents, just that. When a document was opened, its data was represented by objects which were either read from the file, and so were lazy, either new objects created by the user, and so were fully initialized. So the storage of a single property could be either normal, either lazy, and in both cases I needed KVO.

According to the proposal, KVO delegates may be handled in the future.

With protocols we can make operators to make those combinations, but without them we're left with glue code.

+1 for the proposal, with one caveat - I'm not a fan of the $myVar.prop syntax.

Conceptually it seems to me that "myVar" is the thing, and "myVar.etc" are the properties. The $ signifies that the variable name refers to something that's related to, but not actually my variable, which seems odd.

myVar.$prop would be preferable to me. I didn't see it mentioned in the 'Alternatives considered' section, although I don't know if it would conflict with other language features.

myVar.Lazy.prop ?

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

A quick reading.
Thanks.

1 Like

+1. This feature will allow me to remove a lot of boilerplate code. I also really like how it transformed through two pitch threads. Now it feels very swifty.

I think we need another round of revision on this feature before it’s ready for prime time.

• • •

First, the compiler-synthesis aspect is essentially sugar to reduce boilerplate. We can already write computed properties that trivially forward access to another property as the proposal describes. Sure, doing so manually takes a few extra lines of code, but it is certainly a pattern that people can employ today.

So…are they?

Is this pattern in common use? Do major projects define their own UserDefaults type in this manner? Or DelayedImmutable or CopyOnWrite or Atomic or any of the other examples shown in the proposals?

If the pattern of encapsulating behavior in a delegate type is in such high demand to warrant special syntax and inclusion in the standard library, then I would expect to see it in regular use already even without the sugar.

• • •

Second, although the proposal calls itself “property delegates”, it does not actually enable programmers to delegate one property to another. Instead, it is shaped much more like “property behaviors lite”.

The dollar-sign spelling for the delegate seems entirely magical, and goes against the existing precedent of using an underscore. I would prefer to see the synthesized property either spelled with an underscore and always private, or else invisible, unnamed, and inaccessible.

In the case where a different name or visibility is required, I would expect it to be possible for a programmer to manually declare the delegate property, with some way to indicate that the main declaration delegates to it. After all, if we are going to introduce property delegates, the core feature should be providing a convenient way to delegate one property to another.

• • •

If instead what we actually want is property behaviors, then we should start with a clear roadmap so we know where we want to end up. It seems clear that access to self is important, and it is not part of the current proposal.

The “future directions” section talks at length about making self available, which indicates that the authors recognize its importance. Yet the proposal still does not offer that functionality.

I do not think we should pursue this in fits and starts. Instead, we should prepare a single cohesive vision for it. Perhaps multiple proposals will still be required, but regardless of that we should know where we are planning to go.

• • •

Furthermore, with a significant change like adding custom @ attributes, I strongly believe we should have more time with the toolchain so that people can try out the feature, gain real-world experience with it, and work through the kinks.

I would not want to review this feature until I had the opportunity to use it hands-on for an extended amount of time. After all, there could be many different designs in this space, and we should be highly confident we have found the right one before permanently adding it to the language.

9 Likes

I am very much in favor of this proposal. It will provide a powerful and unique way to encapsulate common behaviors, eliminating large amounts of boilerplate.

I think we'd all love it if the proposal had a wider scope that could handle more use cases, but I think you've reached a natural stopping point for a single proposal and we can add more features later. In the Swift 3 cycle, we let the perfect be the enemy of the good. We can't do that again.

My criticisms are all with the skin of this proposal:

  • I still don't love the leading $. If we can manage it, I think an @ would provide a nice visual link between the attribute that applies the delegate and the use of the delegate in code.

  • I still don't love the name "property delegate" and the attribute and property named with that term. "Delegate" is already a fairly overloaded term in Cocoa programming and I don't think it does a great job of conveying what the proposal is about. "Property wrapper" might be a good alternative, but there are many others.

  • I think we should take some syntactic inspiration from the@compileTimeAttribute proposal on the horizon. Maybe something like @wrapperAttribute(declarations: .property)? This would make the two proposals feel of a piece while also leaving obvious space for @wrapperAttributes for methods or types in the future.

If these complaints seem shallow, that's because I think the bones of the proposal are excellent. And even if it's accepted exactly as it is, it will be a huge win for Swift users. I'm excited to see what people do with property delegates.

13 Likes

Yes. I'd greatly benefit from this sugar in my projects.

1 Like

I’ve been using this pattern regularly since the beginnings of Swift. Shared<Value> and Weak<Pointee> made their way into open source in 2017. One wraps a value type in a shared reference; the other wraps a reference type in weak storage (for use in collections etc.). This proposal won’t quite sugar them into oblivion until some of the future directions are pursued, but it already goes a long way in reducing boilerplate.

4 Likes

+1. I find myself identifying with Daniel's review.

I am terrifed that, if this proposal is accepted as is, a day will soon come when I see, for example, reactive codebases litered with $ signs (the BehaviourRelay example in the proposal writ large). It terrifies me because I love the elegance of Swift code.

On a scale between property storage and property behaviours, I'm concerned the elegance of Swift will be ruined by libraries leaning too close toward using this feature for property behaviours, where the only way to access those behaviours is by the internally scoped $property. It's jQuery all over again.

On the other hand, I suppose the library could expose the storage using a cleanly named computed property, but we're lazy humans. Perhaps synthesizing private property delegates could help combat potential abuse within a module?

4 Likes

Sorry for the late comment, but I really like the following proposals and would add them to my initial reply:

  1. “Property Delegate” → “Property Storage”
  2. $propertystorage(of: property)

Thanks in advance!

+1

I’d prefer a different name than propertyDelegate... the already mentioned propertyStorage seems reasonable.

I'm also concerned about the overlap of property delegate attributes and other custom attributes (if accepted) - what if there is some custom attribute Foo and a property delegate Foo in the same module? What does @Foo var something = 14 resolve to? IMO delegates should not share the namespace with other attributes. I suggest @propertyDelegate(Foo) to disambiguate. This would also make it easier to spot a property that the developer is trying to attach multiple delegates to, which is currently not supported. This is more verbose but I believe a reasonable tradeoff on the clarity/brevity range.

Given the issues people is having with the naming of "delegate", would it be reasonable to call this "Property Behaviours" (first iteration) anyway? That would make sense to me if we consider this a first iteration and we keep pursuing the full set of functionality we were expecting from behaviours in the initial conversations.

1 Like
  • What is your evaluation of the proposal?

+1 for having property delegates
-1 for future-proofness

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes!
Having @Observable alone would be worth it.

  • Does this proposal fit well with the feel and direction of Swift?

I think it is okayish.

Please plan for a future multi delegate syntax: What would it look like with the @-syntax to call Lazy’s func foo() (instead of Observable’s func foo())? Maybe self.x@Lazy.foo(). (I actually like this, especially if self.x.foo() is valid if it is unambiguous.)

With this in mind, underlying storage should be hidden and be replaced by callbacks for the get/set methods:

@protocol Observer { … }

@propertyDelegate class Observable<T> {
   var observers: [Observer]

   init(_ initialValue: @autoclosure @escaping () -> Value) {
      self.observers = []

      // Note that the compiler translates this to
      // <next property delegate in order> = ...
      // ending with self@PropertyDelegate.value = ...
      self = initialValue()
   }

   func register(_ observer: Observer) -> Void { … }

   // Since all @propertyDelegate implementations pose as value,
   // var value:T is not required, why not make the get/set part of 
   // @propertyDelegate?

   get { (value: () -> T) in return value() }
   // mutating get { (value: () -> T, store: (_:T) -> Void) in return value() }
   set { (newValue: T, currentValue: () -> T, store: (_: T) -> T) in 
      let oldValue = currentValue()
      observers.forEach { $0.willChange(from: oldValue, to: newValue) }
      store(newValue)
      observers.forEach { $0.didChange(from: oldValue, to: currentValue()) }
   }
}

So for @Lazy @Observable var i: ThickObject, the compiler would create something like:

var $i: DelegatedProperty<ThickObject, Observable<Lazy<ThickObject>>>
var i: ThickObject {
   // mutating because one of the getters is mutating
   mutating get { return i@Observable.get(value:
                   { return i@Lazy.get(value: { return $i.get() },
                                        store: { $i.set($0) })
                   })
                }
   // set …

($i would become an invisible implementation detail.)

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

Annotations/AspectJ in Java.
Property delegates are a bit limited in some regards (only one delegate per member, no "marker-annotations") but seem equally powerful (but easier to reason about) in others (intercepting setters).

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Middle

It looks like custom attributes are converging on “just being specially annotated types” so you wouldn't be able to have a custom attribute and a property delegate with the same name in the same module, just like you can't have two structs (or a class and a struct) with the same name. Unless it happened to be the same type acting in both roles, which would presumably be banned unless it both made sense and wasn't ambiguous.

2 Likes

+1 (huge of course) to this proposal. I've been following these pitches from the beginning and I've had a chance to dig into the toolchains. This is an incredibly useful feature and I think the syntax chosen is great.


One important aspect of using $ prefix for accessing the property delegate is that you can create a key path. AFAIK, this is not possible if we use a function wrapper. For example:

struct Foo {
    @Lazy var bar: Int
}

func delegate(_: KeyPath<Foo, Lazy<Int>>)
func property(_: KeyPath<Foo, Int>)

delegate(\.$bar)
property(\.bar)
5 Likes

I don’t know that I can plus- or minus- this one.

I think the feature in general is a great positive move for the language. However, I think there needs to be more revision and/or consideration on certain areas.

A key and obvious use case for this feature (so obvious it’s mentioned at length in the proposal) is Lazy, however as mentioned, lack of self access cripples this and other use-cases.

Another concern raised in the discussion was composability. Also waved away as something to solve in the future.

I also still feel very strongly that there needs to be more consideration of the visibility of storage. I and others raised this concern in the pitch thread, but it seemed like because there was one or more cases where openly accessible storage was desirable, the concern was dismissed with “we’ll figure out access control later”. Given how module-focused (ie: badly for app developers) the general access control evolution turned out (seems Swift will never have a visibility boundary between module- and file-scope), I have strong doubts that will happen, despite the fact that in this case, exposure of storage can be actively harmful to state control and ability to reason locally.

If this is indeed intended to be the first in a series of iterative proposals and these issues are planned to be addressed in future evolution proposals, then we should be able to gauge the overall direction before committing to this particular syntax and approach (not just in the form of a “Future Directions” foot note). Otherwise, we have to assume that this is it, and we won’t see any of these concerns addressed.

And if that’s the case, then I can’t really support it, and will not feel safe using these features extensively in our codebases, but can understand that others will see value in them.

I will STRONGLY +1 the need for an “experimental” pre-release toolchain for longer-term exploration and trial of these kinds of features before they are permanently added to the language. But this only would be helpful if features sat in this state for long enough for thorough trial... doesn’t seem like a month is long enough to me.

8 Likes

I think maybe we could add a [delegate:] subscript to all types, like [keyPath:], so you can get the delegate for a property from a kay path:

foo[delegate: \.bar] // -> Lazy<Int>
2 Likes

I am a huge fan of the idea of property delegates. I also think this proposal is headed in the right direction. I also think the choice of making delegates a user-defined attribute is the right approach.

So I wish I didn't have to say this, but unfortunately it does not feel fully baked to me yet. I agree with previous reviewers who have suggested that we should continue refining the design before we accept this proposal.

I think it's important to keep in mind when evaluating this proposal that it is basically syntactic sugar with relatively small constant upper bound in code savings.

Without this proposal we can (and many of us do) write storage wrappers. When we do this we can choose to write .value or to write a forwarding property. Explicitly writing out the backing storage and a forwarding property is not fun but it is also not that bad. We should provide sugar to automate this pattern but we should also have high confidence that we are committing to the right solution.

Since it is possible to write storage wrappers manually I am surprised that the proposal makes no accommodation for a frequently voiced concern that backing storage is typically an implementation detail that is private. A syntactic sugar proposal like this one should acknowledge and support widespread use cases where the pattern is written explicitly.

Further, the most recent draft of the proposal introduced a new notion of delegateValue. I think this feature is very interesting, but it also feels like something that needs more motivation and design. It isn't clear to me why the designs of the examples provided that use delegateValue are superior to designs that don't require an additional layer of magic.

Why doesn't CopyOnWrite just rename delegateValue to valueand use private backing storage? Why should its users need to say$foo` in order to write? What is the perceived advantage of this design?

Why doesn't Box just expose a ref property? What's wrong with having to say $foo.ref? Or why not go further and collapse the two into a single Ref delegate. What advantage is there for users to have to think about both boxes and refs when this distinction isn't strictly necessary?

Overall, I think this feature needs to be motivated by examples whose design is clearly superior to the alternatives.

On a design front, delegateValue breaks out-of-line initialization. I think this is a problem and may indicate that we could find a better design for the feature itself and / or the delegates that might consider using it.

The most interesting thing to me about delegateValue is that it allows you to completely hide the delegate itself from user code when there is no useful interaction available for users. However, this requires using a Void delegate value. Having $foo of type Void isn't really desirable. And it is especially not worth breaking encapsulation to get it.

If there is no meaningful API for users of a delegate other than value there is no reason to have $foo available at all. One direction I suggested we might consider is not even synthesizing a $foo backing storage if the delegate does not implement delegateValue. This would require delegates to opt-in to having a visible backing storage. The flaw with this design, as with the proposed design of delegateValue, is that it breaks out-of-line initialization.

One interesting idea that came up late in the discussion and didn't receive much attention is to not expose the backing storage via $foo at all. Instead, relevant API would be available directly on the delegating property with `$-prefixed identifiers. This direction should be explored further. It would require further refinement but may be a better user-facing presentation of the delegate's behavior. One reason we should consider this direction is that autocomplete could support it, exposing the delegate's API in the place programmers would prefer to see it.

If the delegate's API is visible on the primary property there would be no reason to expose the backing storage to programmers at all. Again, this breaks out-of-line initialization. I think if we're going to consider any of the designs that hide the backing storage (including delegateValue as proposed) I think we need to look harder for a way to still support out-of-line initialization of the backing storage. I believe the compiler is smart enough to be able to distinguish initialization from mutating assignment. Perhaps we should consider syntax that is only available for initialization and not otherwise available for reads and writes.

Beyond the topics above, I think there have been some interesting new suggestions in the review thread. Among others, @beccadax has a few that I think are worthy of discussion.

So while I very much want to see property delegates become a part of Swift and I do not want to see this topic deferred for another 3 years, I do think we should send it back for another round of design discussion. I hope we will be able to do that without letting this iteration die completely.

22 Likes

My proposals for alternate spellings:

  • Property Wrappers
  • Property Modifiers
  • Property Decorators

Even with the current naming, I'm +1 on the proposal!

3 Likes