SE-0258: Property Wrappers (third review)

I think that this is only makes it into semantics with frozen types. At the very least, some amount of opt-in has to occur for this to actually make it into the land of user concerns.

@Douglas_Gregor more feedback incoming.

I think we should generalize the following three rules further:

  • The wrappedValue property and (if present) init(initialValue:) of a property wrapper type shall have the same access as the property wrapper type.
  • The projectedValue property, if present, shall have the same access as the property wrapper type.
  • The init() initializer, if present, shall have the same access as the property wrapper type.

None of there rules, except the part about wrappedValue, should require same access level as the wrapper itself.

  • The author of a wrapper type should decide wether or not the client should have access to any of the wrappers initializers. If non are present then the client shouldn‘t be able to wrap his properties with that particular wrapper, it might be an implementation detail or artifact that the wrapper type itself must leak to the client as it‘s potentially used somewhere as a normal type in public API‘s. In the same spirit the author might want to implement an internal init() with some special default while my client should only ever wrap his properties using the public init(initialValue:). You can invert the access level of the previous example as the author might want to expose init() but keep init(initialValue:) internally. Therefore there is no need to require any initializer to have a particular access level, that should remain the wrapper authors decision.

  • I mentioned upthread that with the current design we can safely allow retroactive extensions of projectedValue but at users risk that the wrapper author might add his own implementation of that property in the future and break users code (this is true for any extension of types that the user does not own). I think we do not need projectedValue to have the same access level the wrapper. On a public wrapper the projectedValue might be an implementation detail that should only generate projections internally. This still plays well with future access_level(projection) that can be used by the module author to limit or expose specific projections explicitly.

Both access level restrictions are unnecessary limitations that limit the flexibility of wrapper usage. If there are no inference mechanics involved like for wrappedValue then we should generalize these rules further and make these parts of the type behave literally the same way they behave today.

2 Likes

I really miss constructive discussion on Swift evolution in general - or at least what I consider to be constructive discussion (which for me depends on having arguments and counter arguments, and arguing against the points someone else actually made, and not throwing around random claims and opinions).

Given the rhetoric and my interpretation of the last answer, it's hard for me to keep assuming good faith, and I want to explain what I consider to be problematic in detail.

Simply rejecting everything would be very harmful for progress
 but who spoke about rejection here? The critique did not target the concept at all, but rather the details of the current incarnation!

We really shouldn't listen to those naysayers who outright oppose progress without reason
 but there has been a solid explanation for the skepticism (increased complexity, to name one downside), which you ignore completely!

It's futile to oppose change because of future problems which may never become real at all
 but we aren't talking about some vague future - in fact, you have supported your preference with the common use of the underscore-prefix in existing code!

Now that's a strong argument, isn't it? Of course, you can speak only for yourself - but everyone who's familiar with science ought to know that a theory where you can't find any counter examples is likely to be correct, right? Many people tried hard to construct a perpetuum mobile for a long time, and their lack of success is a strong indication that the laws of thermodynamics hold - and your opponents keep failing in the same way to fulfill your humble request you have repeated several times.
So when there's not not even a single concrete example to prove you wrong, your theory is probably correct, isn't it?
But there's one tiny but fundamental flaw here: This isn't about a theory at all! Just imagine Core would accept all proposals where no one has a concrete counter example:
We would soon have BEGIN and END as alternative to curly braces, unless as alternative for if, and probably hundreds of other small additions.
Staying in the context of property wrappers, we would not only have _foo, but also $$foo, foo_, wrapperOf_foo, foo_wrapper, #wrapper(foo) and countless other variations - or can you show a single, concrete example which proves that all those choices are worse than your personal preference?

It's very tedious to have a debate at that level, and I probably will just not continue it... so if you come up with an argument, and this is actually convincing me, I'm going to admit that explicitly - but when I stay silent, that will just mean my reservations are as big as before.

4 Likes

First of all you‘re right that the current proposed form is my preferred syntax for the backing storage, but I want to make sure that I do not say that "I do want this form and not any other".

Now that this is clear, all I wanted to know is why other do raise concerns regarding the the currently proposed form. I personally don‘t have the same concerns so I would like to understand why other people think here differently. That also means I don‘t want you to prove me being wrong in anything, rather as you said it yourself convince me or at least explain to me more appropriately why you or other think we‘re going into the wrong direction. I do admit that my response was strictly too theoretical but there is still the same point behind it that I tried to make.

In the previous iterations of the proposal the backing storage was prefixed $ and most people didn‘t liked it. Now the design shifted to a state that everyone is familiar with, at least at some certain degree, but again there are opponents of the current and already rather simple design. It was mentioned several times by proposal authors that there are multiple requirements for the backing storage. First, it must have a valid identifier as in the future we should be able to iterate over stored properties. Second, the backing storage should be always accessible, at least in private declaration scope. These requirements imply that the compiler can‘t just fully hide the backing storage and we must find an agreement on some kind of a concrete identifier form. All I wanted to know was a constructive explanation why people reject $ form then _ form but are fine with _$ form, but not some naysaying?!

If we need yet another option for an identifier here is one for consideration:

  • Teach the compiler to allow overloading

    property
    

    with an identifier surrounded by back-ticks

    `property`
    

    instead of _property.

Should we also add init(initialProjectedValue:) which follows DI rules as well?

We are not able to reject $ since this is a design constrain. identifiers that start with _$ are not valid today.

You're right, thank you! The implementation is also wrong here w.r.t. private.

I consider it a benefit that _foo may clash with an existing member. If there's already a _foo and you're adding a foo... and they're not related to each other... it's likely an indication that you've made a mistake somewhere.

Part of the motivation behind using _foo is that it's an established convention already, and it keeps the wrapper simple: the $ only comes into play with the projection, and that only affects a subset of property wrapper types that need that second set of API.

Doug

5 Likes

There is also no indication to the reader that this was compiler synthesized access property to the backing storage. the backing storage is a kind of projection. Of course not a deal breaker and only a problem if the backing storage were to ever become public. My preference is for all projections to share some form of indication that there are synthesized and not user declared.

Somewhat of a nit, but the storage is not being “projected” into the API of the client. It is a real, stored property of the wrapper client and is private, so the client will already know that it is a synthesized property. Furthermore, that it was synthesized is IMO not that important. Understanding that is is backing storage for a computer property is the more important aspect, and that is communicated by the convention of _foo.

2 Likes

While it would be tidy, keep in mind that $ can't just mean projection, since we already have $0, $1, etc. What it can mean for Swift, though, is something along the following lines:

Put more broadly, $ could be used to mean "something you didn't name explicitly, but exists by virtue of something else that you wrote."

Just like some closures (but not all, depending on how they're declared) cause $0, $1, etc. to exist, some property wrappers (but not all) wrapping foo cause $foo to exist. Similarly, just as the type and meaning of $0, $1 varies by closure, the type and meaning of $foo varies by property wrapper.

It would be consonant with such a scheme to name the backing storage $_foo, where $ suggests that it's not a user-named value and _ echoes existing convention for backing storage.


Not necessarily a mistake; it could be legitimate for the same reasons that @Chris_Lattner3 urged us to consider the case when more than one projection is desired:

While _foo is often used for backing storage, it can also be used in the very similar but distinguishable sense of an internal value related to foo in some way. Pretty much like how the proposal eventually evolved from $foo exclusively being the name for the backing storage to its indicating a custom projected value.

I don't think we need to categorically deem it a mistake for a user to want to have a manually created ad-hoc projection for foo named _foo simply because a property wrapper may or may not provide a projection named $foo.

3 Likes

This would not work as a $ variable name can only be defined/declared by the compiler.

Unless we have a story for propertyWrappers, projections and protocols I would hesitate to give a +1 on the proposal.

No more that it would work with your proposition as the @projected() attribute is not supported by the compiler.

We are talking about new features, so answering it would not work because the compiler does not support it is not really a convincing answer.

This would be somewhat true if the backing projected by property wrapper always matched the type of the backing storage.

@Douglas_Gregor

In the proposal, this part of the code suggests that Lazy’s wrappedValue is Self but the code immediately above shows wrappedValue as the generic Value.

extension Lazy {
  /// Reset the state back to "uninitialized" with a new,
  /// possibly-different initial value to be computed on the next access.
  mutating func reset(_ newValue:  @autoclosure @escaping () -> Value) {
    self = .uninitialized(newValue)
  }
}

_foo.reset(42)

Similarly, DelayMutable shows a reset method that can’t not be accessed via any projection.

I’m wondering if I am misunderstanding something.

The 2 examples you quote (Lazy and DelayMutable) don't have projection, so they don't project anything.

1 Like

I think it is clear that I meant your approach would not work if we stick to the principle that only the compiler is allowed to introduce $-prefixed identifies (also in protocols).

Also it does not help if you want to expose the base property and it’s projection at once.

I think this comment highlights my concern. Treating the backing storage as if it is not a projection makes the concept hard to even talk about.

We could support a similar syntax for protocols:

protocol MyProtocol {
    @MyWrapper var myVar: Int { get projection }
}

would then be interpreted as

protocol MyProtocol {
    var myVar: Int { get }
    var $myVar: MyWrapper<Int>.ProjectedValue { get }
}

I.e. if projection is used next to get/set together with a property wrapper, then a new { get } property would be added to the protocol based on the type of the projectedValue property.
The type of the wrapper would not necessarily have to be encoded if the protocol. E.g. when the type of the projected value is of some common type, then another property wrapper with the same type of its projected value could be used to conform to this protocol.

3 Likes

What is your evaluation of the proposal?

The improvements in this third evaluation and fairly minor but welcome. I stand by my very positive opinion of this feature and the expressivity in brings to the language. I'm specifically a big fan of the fact this feature can be used both for "active" wrappers which modify the property accessor behaviour and for "passive" wrappers which allow attaching metadata to properties and accessing them at runtime.

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

Yes.

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

The fact that this feature uses the @ syntax that has been used for compiler-defined attributes makes this proposal fit very well in Swift's current syntax. It also improves the use of wrapper types which has become quite common in several frameworks.

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

Kotlin seems the closest language to have a similar feature but I haven't used it there.

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

In-depth study and implementation of several property wrappers.

There currently isn't a plan here, although the proposal could be extended with either of your suggestions. Personally, I think Ithe second suggestion you make (allowing one to declare a requirement with the $ name) is more true to the model---that's the set of requirements that matches up with what the compiler generates for a wrapped property.

Doug

2 Likes

Yes. I think we fixed it with [5.1] [SE-0258] Trigger synthesis of _foo/$foo from name lookup. by DougGregor · Pull Request #25821 · apple/swift · GitHub but it'd be good to double-check.

Doug

1 Like