At some point, you have to ask "if the feature was locked down to what it is today, and I couldn't ask for more and more generalizations and flexibility, how would I design my API, and would that actually be worse?" This feature has three aspects — composition, projection, and initialization — which are interacting with combinatorial complexity for the design and implementation. It is already quite clear that several significant aspects of the current design here are going to have to be left unimplemented for some time; I think we need to be very cautious about coming up with new ideas that are going to worsen that problem. There will be opportunities to extend this feature in the future if the current design proves inadequate for some use case.
@Lazy var foo: Int
declares two things: a stored property _foo
of type Lazy<Int>
, and a computed property foo
of type Int
that's just an alias for _foo.wrappedValue
. _foo
is a reference to the former, and the reset
is just an ordinary method call on it.
It occurs to me that I haven't been able to deduce what your MyWrapper
type looks like. I think it's something like:
@propertyWrapper
struct MyWrapper<Value> {
var wrappedValue: Value
var projectedValue: String
}
... but I don't know what that's useful for, or what it should do when combined with @State
.
SwiftUI's @State
does need to be the outermost property wrapper for it to work properly (because the stored property needs to be a State
instance; that's how SwiftUI finds it). Having the projection be from the outermost wrapper (so it produces Binding<MyWrapper<Int>>
, as you noted) does the right thing from that perspective.
We could probably find a motivating use case where picking the innermost wrapper type's projectionValue
is the right one, although I haven't seen one with enough semantic context for us to make a judgment call.
Aside from it being the right behavior for the SwiftUI example, synthesizing the $
based on the projectedValue
of the outermost wrapper type is a conservative step that we can build on with later iterations and more experience. For example, we could extend it to "if exactly one wrapper type has a projectedValue
, use that for the $
property", or take @anandabits idea:
We have a safe default design now and can extend it in the future, once we've built up more experience with the feature.
Doug
I guess I understand the intended behavior more clearly now. Thank you for mentioning that SwiftUI needs State
to be the outer most wrapper so it can be dynamically detected by the framework, which is clearly a potential requirement for to have the default behavior project the outer most projectedValue
properties. And I'd like to thank @anandabits for his idea which strengthens the design for the current behavior a little.
I am in favor of this proposal in its current form. In my opinion most if not all of the important issues from previous revisions were addressed. $
identifier still feels a bit weird to me and not too "Swifty", but I don't know of a good alternative. As far as I understand, this is not something that can be changed at this point?
Yes, absolutely, the examples given in the proposal, use of this feature in SwiftUI and Combine make it very clear this feature will simplify a lot of code and opens a lot of opportunities for API design.
The proposal itself does fit well, except the $
issue mentioned previously. I understand the reservations some people voice about "custom attribute" abuse feeling too "Java-ish", especially with nested wrappers. But I don't know of a good alternative to this, other than continued use of nested generics and duplicated boilerplate code. I hope that these reservations are more emotional than rational and they will go away as soon as this feature settles and we can start reaping the benefits.
As far as I understand, the closest similar feature is available in Kotlin, but I'm not familiar enough with Kotlin to make a fair comparison.
I followed all of the previous revisions and review threads, albeit in read-only manner.
A little bit late to the party, sorry if this was previously discussed (I went through all messages in this thread, but I might've missed it, or it might've been discussed in the previous threads).
I have a question about projections. Basically if the wrapper type is the one that gives the access level for the projection, does this mean that any clients will be able to access it, if the property is accessible enough?
For example, using the Lazy
wrapper from the proposal:
class MyClass {
@Lazy var foo = 1738
}
let obj = MyClass()
// can I mess with the lazy storage from the outside? e.g.:
obj.$lazy.reset()
Is it possible to keep the wrapper details hidden from the clients of my class, but to expose the property alone?
As far as I understand, this depends on how you define the projected property, specifically in the latest revision there's this section:
This is how I understood it too, does this mean that I cannot use an existing wrapper that exposes a projection, assuming I don't want to expose that projection, but I want to use the features the wrapper adds?
It would be nice if the wrapper client could decide to give access or not to the actual wrapper, as it might not always be desirable. In the "lazy" example, if the Lazy wrapper provides a projection I might not want for consumers of my class to be able to reset the property.
@DevAndArtist I think it was around Pitch 3 when I pushed for the wrappedValue
and projectedValue
to both be projections from base storage. Both were on relatively equal footing back then (similar to now).
Nevertheless, I was also thinking that it'd be useful when projectedValue
is not a one-off type, but normal type that is useful outside Property Wrapper itself (think UnsafeMutablePointer
mocking example in the proposal). So that we can have the value be created outside, then consumed by the wrapper. It would be rather common for boxing types.
I was the one pushing for it in the beginning, but I now think current design ($
refers to outermost one only) is a good one. The more I think about how composition works, the less I think accessing inner projectedValue
is a good idea. It bypasses outer wrapper and uses internal value directly. Those outer wrapper will likely want to know when other parts of the program access the wrapped value, which is not the case when we directly access inner wrapper directly. It's pretty similar to Law of Demeter at play here.
What does accessing a property wrapper between frameworks look like? What if there’s a name collision?
@Framework.PropertyWrapperName ?
This has come up before. Most likely you'll want to put reset
inside the ProperWrapper (Lazy
) instead of the projected value (Lazy.projectedValue
). So that you'll be able to access it by _foo.reset()
which is strictly private
.
It's also mentioned in Future Direction to have finer grain access control.
This was brought up somewhere in Pitch 3, or Review 2. IIRC that's exactly how. @Douglas_Gregor could probably clarify, and add clarification to the proposal.
The thing that comes after the @
is just a type name, so it can be qualified with the name of the module like any other type name.
Correct: if a wrapper type provides a projection, you can't hide the projection under the current proposal. Most wrapper types are not expected to provide projections. In the future-directions section, there's a suggestion that we could allow something like private(projection)
to explicitly control the access of the projection.
As projectedValue
is optional, it would be hard to get an intuitive behaviour anyway. If @Bar
does not have projection (and we use the inner type projection):
class Foo {
@State var prop1: Int // has a storage and a projection
@State @Bar var prop2: Int // doesn't have a projection
}
That is correct and that is what I have expected first, but as I mentioned upthread I now better understand the reason why we should project projectedValue
from the outer most wrapper.
I would go as far as to say we definitely don't need property wrapper composition at this stage. The use cases seem very few, it is far from obvious what it would even mean, at least to me, and you can always put the combined behaviour into your own property wrapper type.
If composition delays property wrappers I think that whole discussion should wait. But of course I'm biased since I don't see myself ever wanting to compose them.
I agree with @GreatApe that if we're not absolutely sure this is how composition should look like, we could wait.
Meantime you can build a custom property wrapper and compose it with a different wrapper to prevent leaking the projection.
protocol ProjectingWrapper {
associatedtype ProjectedValue
var projectedValue: ProjectedValue { get }
}
protocol MutableProjectingWrapper: ProjectingWrapper {
var projectedValue: ProjectedValue { get set }
}
@propertyWrapper
public struct OpaqueProjection<Value> {
public var wrappedValue: Value
}
extension OpaqueProjection where Value: ProjectingWrapper {
var _projectedValue: Value.ProjectedValue {
get { ... }
}
}
extension OpaqueProjection where Value: MutableProjectingWrapper {
var _projectedValue: Value.ProjectedValue {
get { ... }
set { ... }
}
}
Something like this.
Hmm, NoopWrapper
is quite an interesting way to tackle this.
I just posted a concrete, real-world example earlier today that requires composition. There is no way to build this on your own because SwiftUI has specific knowledge of ObjectBinding
. I don't think this is an isolated example. Ad-hoc composition may be few, but I think composition of wrappers intentionally designed to compose opens up a very interesting design space for library authors.