SE-0258: Property Delegates

This isn’t true as recently as SE-0228. Having a named protocol in the stdlib even if it can’t describe its requirements there is far preferable to me. A protocol gives users something to Cmd-Click on, where they immediately see the feature in the larger context of the language feature as crafted by the Core Team and Apple’s technical writers. This has immense value for people learning of the feature. That property delegates are pitched as something a Swift user might use daily has me expecting absolutely nothing less.

I’d characterize the recent wave of @magic additions as having unintended negative consequences. For instance, the adoption @dynamicMemberLookup in the community would’ve been well-served by even the slightest of documentation gently guiding them with, “Use this feature [only] if…” Instead, many codebases I’ve seen adopting it are suffering from the worst kinds of cargo-culting by cobbling together several “Wouldn’t it be cool if you could…?!” blog posts into major parts of their code.

7 Likes

If we get compile-time attributes, Cmd-Clicking an attribute could pull up the documentation for the type implementing that attribute. It's one of the underrated benefits of this trend towards representing attributes as types.

3 Likes

I apologize for the off topic post, but it's sad to hear that the concerns I had about this feature are coming to pass on some teams.

3 Likes

I’m a strong +1 on this problem space, and following precedent from other languages to accomplish it is icing on the cake.

Though I have major problems with the declaration side of property delegates, the syntax it produces at the call site is distinctly Swifty, and for that I’m excited.

I could see myself being happy if this formulation of custom attributes were the only version, to the exclusion of runtime metadata-based annotations; with no offense intended toward that pitch thread.

Not solving for Observable<T>, the bread and butter of this feature in Kotlin, is mildly disappointing. I do not like the look of the direction in Future Directions and feel like we’re missing a more elegant solution somewhere.

I have misgivings some of the nitty-gritty specifics:

Access control

While I am sympathetic to not wanting to create an unreviewably-large proposal, not having any answer for access control at all — a feature that has been around since the first betas — tells me that overall fit with the language hasn’t been considered very much, and isn’t acceptable to me as a first iteration. As a compromise, the first implementation should have the storage be private, if accessible at all. We can open up access later, but closing access later is a source compat risk.

Spelling and mystery meat attributes

See my reply above.

Sigil problems

I don’t find $ nearly as controversial as others do, but it seems clear that it’s causing heartburn. Prefixing the methods on the storage seems like a sensible compromise. I also might suggest #, which already has precedent in the community as “shorthand for the compiler inserting some deeper access”. propertyName.#accessor(…) makes sense to me, though admittedly doesn’t compose with subscripts fantastically.

1 Like

Yes, you’re correct - but I’m referring to one level above that, the documentation on constructs like @propertyDelegate themselves.

Learning most of Swift’s features is an act of looking at documentation once you nail down the initial syntax. It’s a big theme when I teach the language.

A couple of other random thoughts:

  • I feel like the first implementation of this really needs to have access to the enclosing self, and later versions can add options without it for optimization-sake if necessary

  • There needs to be a way (at least planned) for delegates to constrain the Types that they can be used with as well as the properties of the enclosing type it can be placed on. I had a suggestion here using associatedTypes, but other ideas can work as well

  • We need to have a plan for how these will compose in the future, even if the implementation will come much later. We don't want to paint ourselves in a corner...

  • We should have a plan to allow parameters in the attribute (e.g. @Delegate(foo: 1, bar: "")). Are those inits? Some other form of declaration? We don't have to implement it now, but we should know where we are headed.

  • We should allow something like @MyDelegate(using: myStorage) to declare custom storage with a programmer defined name. The built-in storage should be unnamed and inaccessible (except for some mechanism for surfacing shared properties on the property). I.e. Delegates should be able to actually delegate...

  • I don't think we can wait to have an answer for access rules with the current design. If the storage were private/fileprivate by default we might be able to, but with internal we will be exposing a lot of unwanted surface whenever we use this feature. Furthermore, when you think about it, the real problem isn't whether to expose the storage or not, but that we really want to expose only a few methods of the delegate and keep the rest private to the enclosing type. That is something our current access system can't quite spell. My suggestion was to have a syntax that exposes specially marked methods to everyone with access to the property, and for access to other parts, you would create the storage yourself. Another answer could be to have annotations which expose some methods externally and others only to the enclosing type.

  • I'd really prefer to see this spelled as a protocol (even if it requires a little compiler magic) with the methods that need to be implemented spelled out. I think it would also make the composition and constraint issues mentioned above much easier to solve.

5 Likes

If the compile-time attributes proposal allows us to declare "built-in" attributes in the standard library—even if the declarations are just stubs with no behavior attached—they could get the same benefits. In any case, I think we're drifting off-topic.

The main issue here for me is that it breaks the mental model. Sure most programmers are good at figuring out arcane rules, but does it feel particularly elegant to you? That may not matter as much for small features, but this will be an extremely well trafficked feature once it is in place, so it makes sense to take a little time to lay a good foundation to build on.

As I said, the mental model we are building here is that we can apply behaviors to a property by annotating them to say that we want that behavior. But if in order to use that feature (e.g. Observe), the user has to know not just that it is Observable, but understand that the compiler has generated a secret variable, and that they need to use that variable instead of the property to do anything with it... well, it breaks the illusion, breaks progressive disclosure, and exposes a lot of the internals unnecessarily. Whatever we do, to external users of a property, it needs to look like something we are doing to the property itself. That is why I suggested having a way to expose certain methods so you can say: myObj.foo.$observe {...}. I am doing something (observing) to foo itself. I do still need to know that it is observable (I can see the annotation), and I need to know about the $observe func (which I can see in autocomplete and I can even see the declaration), but I no longer have to know about the backing storage, and more importantly, I only have access to the parts the designer of @Observable wanted me to...

I think you are hitting on the key issue here, where exposing the whole storage or not is not really what we want. For some use cases (e.g. COW) we want easy access to certain methods in our enclosing type, but we definitely don't want to expose those external to our type. For other use cases (e.g. Observe), the whole point is for external types to be able to say myObj.foo.$observe {...}, but we don't want to expose the rest of the type to them.

I don't think we have quite hit upon the answer yet (I am throwing ideas out there in hopes that they spark ideas in others), but I think we are close.

On the symbols, I agree with you in that I would rather not burn $ at all. That said, if we do decide to use it, what I would really like to see is it used in a way which can be built upon for other features in a similar space. The current design burns the entire space of using dollar-sign prefixed things because we won't want to limit the names people give their properties. What I like about the post-fix operator is that it only burns the post-fix operator use, but I'll admit it isn't the only (or even best solution).

I'd much rather see $ used in a way which consistently means something like meta-access, or accessing something at a more basic level (e.g. myVar.foo.$reset()). If instead of just representing "backing-store", the $ meant accessing the structure of the property itself, well, then things start to get interesting. You can then easily imagine other features which also provide access to that higher level of other constructs.

5 Likes

Hi @John_McCall, @Douglas_Gregor, and @Joe_Groff

  1. What do you think about allowing people to declare type variables? That would solve some problems here. For example, instead of @UnsafeMutablePointer @Atomic var x = y being problematic (because attributes are unordered), one could write var x : Atomic<UnsafeMutablePointer<_>> = y where _ in a type context means "type variable".
  2. How likely is it that the reference storage types could be implemented (or ever be implementable) as property delegates?
1 Like

This isn't actually my proposal; I'm just the review manager. But I can answer one of those questions: weak and unowned could certainly be treated as sugar for property delegates in the future, but we'd need a general ability to define custom copy-constructors and destructors in Swift.

7 Likes

I agree with two points @Jon_Hull makes:

  1. we should not burn entire $ namespace just for this one feature. The concept of using $ for meta access that Jon proposed is more appropriate way to use $.

  2. backing storage value and the API that users should be allowed to use are not same. I very much like the idea of explicitly defining methods that can be used on storage. So you’d, for example, use them via myProp.$methodName instead of having unfettered access to the whole storage.

EDIT: which leads to @ptomaselli’s point: if we end up re-producing all the features and syntax of structs/protocols (including access control) in the context of property delegate, then I don’t think this is worth it. Just use wrapper types or some other less complex solution.

It makes me feel like a grouch, so apologies in advance, but

-1

I get the sense this might be a minority opinion :slightly_smiling_face:, but I don’t necessarily think avoidance of “wrapper types” is always a clear win. @lorentey and @anandabits have explained this well above.

A wrapper type is often an opportunity to provide a clear, domain-specific API, write documentation, etc. It’s something beginners can read about, click into, and all the usual things. The presence of the wrapper in the type itself provides value, in my opinion, by signaling, in the type, what the behavior is.

This seems like a large amount of new syntax, conventions, attributes, etc. being introduced simply to avoid some forwarding of calls. I would love to have a blessed Observable, UserDefaulting, etc., but I feel like they should just be… types. In the standard library. With regular API!

I don’t know, perhaps I’m missing something. I think that it is telling that there is much discussion of access levels, what the storage should be, etc. It’s because, IMO, this proposal tries to unify many disparate things underneath a single (ad hoc) protocol! It’s no accident that there is no single ultimate API shape suitable for all these different gadgets. Everything we find odd about squeezing all these things into the same-shaped tube will be experienced anew by every user who is interacting with one of these decorators for the first time, IMO.

8 Likes

I second this:

The design space of this feature needs further exploration. Lets not rush this.

3 Likes

This is a really interesting point. When I read this I started to think about the proposed feature quite differently.

We recently had a review about "static callables", for types which implement the function-call syntax. I mentioned in that thread that we might also like some kind of "static member-lookup" for objects which simply serve to forward to another object.

And then I consider this "property delegates" proposal. At its heart, it is a way to have a "transparent" wrapper object, where member lookups are forwarded to some other object. Why is that only important for properties? Wrapper objects are quite common in general - perhaps we should consider a more general interface for transparent wrappers.

Feature creep is always dangerous - it's the famous "artist's dilemma" - when is something good enough to ship? Still, I think the design space is quite large and very interesting, and we should take the time to explore it properly, because it may end up looking quite different to what has been proposed, and this one has proceeded extraordinarily quickly. I even mentioned during the pitch thread that things were moving so quickly that I couldn't quite figure out what the latest situation was or really digest the new features. Maybe I'm just dumb or slow or something, but I like to take my time to absorb and read the proposal several times.

Also, to quickly respond to @jawbroken 's point: it is true that a prototype for this existed some time ago. That said, I don't think it is entirely correct to say there have been "several years of thought and discussion" about this. There was a thing, and it was basically left to one side while the community and dev team focussed on more pressing issues. We're revisiting an old topic, that's true, but it isn't battle-tested by years of experience or anything.

I think I've already made clear my distaste about how quickly this proceeded to review - I don't want to harp on about it. The point of this comment was to note that transparent wrapper types in general are a useful feature, and could in some ways be seen as analogous to C++'s operator -> or operator *.

7 Likes

+1
Seems like logical and useful proposal.

$ identifiers: might be needed to be declared private.

Also, do you plan for "function delegation"?

1 Like

+1

While I'm not fully convinced if the storage should be exposed in the currently proposed way, overall, I like the idea. This is going to totally change the current practice and has huge impact.

Reading a bunch of ideas around $ syntax, one idea I can think of is to expose it by synthesizing a method like below.

mutating func propertyDelegateStorage(block: (inout T) -> Void) {
  block(&storage)
}

Then, we can do:

foo.propertyDelegateStorage { $0.reset(42) }

if thingHappened {
  counter.propertyDelegateStorage { $0.increment() }
}

Obviously propertyDelegateStorage() should be bikesheded and this may be too cumbersome if frequent direct access to the storage will be the norm, but in this way we don't have to introduce new syntax at all :slight_smile:

6 Likes

I was responding to your misleading claim that this idea appeared “literally 25 days” ago and the implication that this was being rushed through in an improper or unusual way. I'm not saying that people have been working full-time on the problem for 3-4 years, but I can say that I read the initial proposal back then and have been thinking about how it might work with various other proposals and leverage new features introduced since then. If you're looking for something battle-tested through years of experience then you want a much higher bar than I do for new proposals, and I don't think such “big picture” changes to process should be litigated in unrelated individual proposal threads.

Right now, all custom attributes are in the same namespace as all other entities, so you'll get the Foo that is visible to normal name lookup. If that's not the one you wanted it, you can qualify it with the module name, just like with any other module-scope name: @LibA.Foo

Doug

2 Likes

Just to be clear, does something like the private(storage) future direction meet your criteria here, or do you feel that there is a deeper problem with the design that needs to be addressed?

I think we can provide a more natural CopyOnWrite wrapper using generalized accessors, since modify is able to do the uniqueness check and efficiently yield the unique value. I've been meaning to try it with the toolchain, because _modify is in the compiler already. Should the property delegates proposal examples depend on this experimental feature? I guess it could.

Huh, I thought I had addressed this. The intent is that Box is for creating storage, and Ref is about referring to something that's either directly in a Box, formed through some other get / set pair (say, a computed value), or derived via key path from another Ref thing.

I liked your idea of doing Definite Initialization for the $ property based on init(initialDelegateValue:). Haven't had time to implement it yet, but it seems like a reasonable addition here.

Alternatively, one could make the $foo private in such cases. That gives you direct initialization and hides it from the outside world.

I'm fairly strongly against this direction, because delegate types are normal types. The "weirdness" should be in accessing the backing storage of the property that has a delegate; once you have that value, it's just a normal value of the delegate type.

FWIW, autocomplete can support this design, too, by inserting the '$' earlier. There's some precedent here with insertion of ? in someOptional.<completion shows members of the wrapped type>.

It is smart enough to do that through init(initialValue:). As mentioned above, it could do the same for an init(initialDelegateValue:).

Doug

Syntactically, @ is used to denote attributes, and attributes aren't expressions in the grammar. We would have to introduce some odd grammar productions to make something like @storage(foo) work. It's also very verbose compared to $foo.

Doug

1 Like