Pitch: Property Delegates

Sorry, it would have been more obvious as min: we want the more restrictive of the two.

Doug

1 Like

I thought that was the intent, but it didn't match my mental model of "more access is greater-than". Thanks for the clarification.

It’s not important to expose it publicly but I think much of the time I would want it to be private or fileprivate while the original property has higher visibility. It is undesirable (to me) to have to expose the storage everywhere in a module for public and internal properties. We shouldn’t have to choose between syntactic sugar and encapsulation.

1 Like

I think the most reasonable default, especially if there’s no affordance to specify it would be private. If one wants to expose the storage, one can always write a more visible get/set wrapper.

I tend to agree, although at that point you've nearly abandoned all of the gains of using the syntactic sugar. The plus side of this approach is that you can choose what part of the delegate's API you wish to forward, such as adding an observer.

I think here we would want property forwarding to kick in to gain parts of the delegation back. Then you can expose the storage and still delegate to it.

I think @davedelong's comment is spot on, and whatever proposal that would implement this, should also consider the bigger picture

If this proposal goes through and some proxy mechanism later, they should be similar in how they work and work well together. Swift shouldn't become a language with a bunch of different things stapled on, it should be consistent and well thought-out.

For this proposal, it might be worth it taking a step back, and consider what we want to achieve long term.

2 Likes

Thanks for your work on this, Doug. I can already think of several use cases for this in my code base - dependency injection, exposing an RxSubject as an Observable externally, etc. My only concern is that now that we're talking about using the "@" syntax (to which I am +1), does it make sense to burn the $ prefix on storage, particularly since it doesn't show up anywhere else in the feature anymore?

A couple other possibilities, off the cuff:

self.$foo = DelegateType(arg: 17) // Currently proposed

self._foo = DelegateType(arg: 17) // Auto-generated, _ has precedence for internal
self.@foo = DelegateType(arg: 17) // Auto-generated, matches the @ in the declaration
self.foo.storage = DelegateType(arg: 17) // Doubt there's a way to make this actually work

I think consistency with the language’s internal default for everything else is important here. Remember, Swift is a language where a single type’s definition may be scattered across many files; since stored properties must be declared only in the type’s declaration, private-by-default means any logic requiring access to the backing storage would have to be in that one file.

Imagine what this would mean, for example, for the CopyOnWrite delegate I described upthread. In that example, the main property was immutable and you had to mutate the value through a property on the backing storage. If the backing storage was private, this would mean you'd have to pile all of the mutation code into the same file as the type's main declaration. Imagine what that would mean for the standard library or another large library which organizes the code in large types into several files (Array.swift, ArrayBody.swift, ArrayBuffer.swift, ...). Will they rip apart their useful per-concern organization to use @CopyOnWrite? Will they rewrap the backing storage property in an ad-hoc fashion and depend on the optimizer to remove the overhead? Or will they not use the delegate at all?

(I also think that the lack of separate access control is a bigger flaw than most of us think—the fact that the backing storages disappear when a type is moved into a module will chafe in practice. Not enough that I think it should be made public, but enough that I think it would be a very good idea to allow it to be made public.)

3 Likes

I disagree, and I'm going to disagree more strongly than is strictly necessary because I think this general pattern of discourse can be harmful to the process, particularly for less-experienced proposers.

The pattern looks like this:

  1. This proposal looks like a form of
  2. Something like could be useful elsewhere in Swift
  3. You should go design all of because your proposal might end up different after that exercise

Abstractly, it's a reasonable line of argument: we want to find the generalization that ties together different ideas, rather than having disparate features that don't compose.

However, when making this "step back and consider the larger picture" argument, it is important to show that there is a plausible larger picture, and what it might look like. That's completely missing here: the entire argument rests on the idea that there is some "proxy pattern" that would subsume property delegates and a bunch of other use cases.

  • Those use cases aren't spelled out.
  • A more-general feature that covers those use cases isn't spelled out.
  • The way in which that more-general feature serves the use cases of the proposal under discussion isn't spelled out.

In fact, there is literally nothing in the post that ties together the feature proposed (property delegates) and the proposed larger picture (proxies) beyond the use of the term "proxy pattern." The suggestion is meant in good faith, but its effect is very different: it's signing the proposal author up for some unspecified amount of work with unclear goals. For a less-assured proposal author, it acts as a roadblock: it's hard to say "no" to a goal that is abstractly good, but impossible to address the comment in a constructive way without exaggerating the scope of the proposal beyond what the author intended (or even cares about).

Contrast this with the suggestion to tie this in with custom attributes. There, there is a clear link ("re-use the syntax from this other pitch instead of inventing new syntax"), a clear "larger picture" (a pitch for the generalization we're talking about), and clear goals ("we only need the attribute syntax and nothing else new"). We've been having a useful discussion about the technical trade-offs ever since, because the suggestion was concrete enough that we can discuss specific details.

Doug

22 Likes

Yeah, another point in favor of internal: the implicit member wise initializer is internal by default, and delegate types can make their way into the memberwise initializer (when there's no init(initialValue:)).

Doug

2 Likes

It would not mean this - you can still write forwarding APIs that expose the backing storage in whatever way you wish. However, if we don’t have any way to lower the access level then internal properties with delegates are forced to expose the backing storage. There is no alternative.

Further, if we do have a way to specify access level then the consistent default of internal in the above case is acceptable. I don’t mind having to write something like private(storage).

I'd like to second this. It seems like many people (me too) want their storage properties to be private/fileprivate. In that case, not only do we need an easy way to achieve that, but if it's the most predominant access level for property delegate storage, that's a strong argument for making it the default.

1 Like

I find the default to min(internal, property access) is a very good tradeoff. I don't mind having my storage exposed for internal properties, but I would find that far more annoying for private and file private properties.

Why is $foo a better name than _foo for the storage of foo?

2 Likes

As long as they don't leak into ABI, I don't think this is the end of the world. Below internal, making something too accessible makes certain mistakes slightly harder (but still very tractable!) to catch, while making it not accessible enough means you need to restructure your code to work around the access control system. I think the second is worse than the first.

I don't think it's fair to call having to write a forwarding API a need to "restructure" your code. It's more verbose than private(storage) but it's not a structural change. On the other hand, it's less verbose than manually implementing the backing storage and forwarding the primary property which is the workaround for lack of ability to have lower access to the storage. Do we really want to introduce sugar that is useless when you want private storage? I hope not.

It would be good if you could set access, but I think the feature can ship without it as a first pass, whereas it would be very limiting if the access was locked to private.

2 Likes

I can't think of any problems with this, but is there a problem with the storage being accessible via mirroring or runtime reflection?

As someone who builds both libraries and apps on a team of developers of mixed skill-levels, I can say that the inability to make storage private while taking advantage of this feature would basically kill its use on our team. We work very hard to achieve real encapsulation in our code because the alternative is endless spaghetti. Exposing storage by default with no way to restrict it would basically make for continuous code-review comments, so the sugar would go entirely unused even when it’s the right tool for the job. This would be hugely unfortunate.

Especially since it would be trivial to expose the private storage via a simple get/set computed property in the case where you wanted it more accessible.

8 Likes