For a property foo
that has a property wrapper, the type metadata will contain (only) the _foo
synthesized storage property, which has the wrapper's type. That's what will show up in reflection.
- Doug
For a property foo
that has a property wrapper, the type metadata will contain (only) the _foo
synthesized storage property, which has the wrapper's type. That's what will show up in reflection.
I would even say that the wrapper/storage type should not show up in a protocol - only the projection type.
The proposal will run through tomorrow, @Douglas_Gregor will you have time to review the rest of the feedback from this thread?
I‘m especially interested what you think about the feedback I posted above regarding retroactive extensions of projectedValue
, then the drop of the strict access level requirement on inits and projectedValue
, and finally a clarification for the correct projection of composed property wrappers.
With the current design I think the mentioned rules are artifacts from older iterations that can be lifted now.
The $
variable uses projectedValue
of the outermost wrapper. That means we'll be directly projecting from the synthesized storage property.
The implementation already does this; I'll clarify the proposal.
Doug
Why are we doing that? The wrapped type comes from the inner wrapper, why is it different for the projection? The compiler should be able to traverse and check the types the same way through multiple calls to wrappedValue
and check if the inner most wrapper has a projectedValue
.
Is there a logical reason I misunderstand here? (I just want to understand the rationale for this behavior.)
There's a philosophy behind these restrictions: a property wrapper type should behave the same way in all contexts, and one should be able to look at the primary definition of the property wrapper type to understand how it's going to behave when used as a property wrapper.
Loosening the access restrictions, or allowing projectedValue
et al to be provided via extensions, chip away at our ability to reason about what a given property wrapper type will do when it is applied to a property. I don't think we should make it any harder to reason about property wrappers just now---as it is, we need to figure out how to better document them and get people used to them.
Doug
The API on the wrapper type itself is accessible via _foo
within the scope where the property was defined (because _foo
is private).
Doug
@Douglas_Gregor I have one question regarding typealias
: Do you think it would be possible to allow property wrapper lookup to include typealiases in the given scope?
This could allow for property wrappers to capture the Self
type, which would be very powerful:
@propertyWrapper
struct ModelField<ModelType, Value>
where ModelType: Model
{
var wrappedValue: Value
init(initialValue: Value) {
self.wrappedValue = initialValue
}
}
protocol Model { }
extension Model {
typealias Field<Value> = ModelField<Self, Value>
}
final class Planet: Model {
@Field var name: String
init(name: String) {
self.name = name
}
}
This currently gives an error: Unknown attribute 'Field'
, even though Field
is known at this scope. For example, I can do this:
final class Planet: Model {
var name: Field<String>
init(name: String) {
self.name = .init(initialValue: name)
}
}
To add on the projection issue from above. Take SwiftUI for example. If I wrap a property with State
I‘ll get a Binding
as the $
projection. Both types come from State
.
Now if I had a custom wrapper which would always project a String
and if I compose it with State
, I will loose the Binding
as the projection but my wrapped type remains untouched, it will just be guarded by an additional behavior of MyWrapper
.
@State var number: Int
// var number: Int { ... }
// var $number: Binding<Int> { ... }
@MyWrapper @State var number: Int
// var number: Int { ... }
// var $number: String { ... }
This seems contraintuitive.
Okay I would say "it‘s okay if these limitations stay in the initial implementation", but I think we should consider lifting them in the future as with the current design the additional behavior that they would give us will still remain predictable. I strongly think that these are artifacts from previous iterations of the proposal and are unnecessary just as it was unnecessary to require a fixed generic Value
like in the very first pitch.
It's partly that I've been thinking of this in terms of the init(initialProjectValue:)
suggestion (see below), which biases slightly toward directly initializing the storage:
That said, we could implement a rule stating that at most one of the wrapper types can have a projectedValue
, and that's the one that will be used for the $
variable. If we do get an init(initialProjectedValue:)
, it would likely work only on the outermost wrapper type.
Doug
Yes, I consider the current behavior to be a bug.
Doug
Awesome, that's great news! Thanks Doug
Should this then be
var wrappedValue: Self
Instead of
var wrappedValue: Value
I’m a little confused. My apologies if I am missing something.
For SwiftUI specifically, you would need the @State
to be first, so the framework can find it... in which case we'd get an error because Int
is not the same as String
.
Doug
Hmm I‘m not sure I follow that idea. @Lantua can you provide an example what you had in mind with such behavior or pin point me to a post I might have forgot about? I currently don‘t understand why we would want to initialize the projected value instead of the wrapped value.
If we're still talking about, e.g.,
@Lazy var foo: Int = 17
Then wrappedValue
is about getting to the Int
(the Value
generic parameter) and you don't need a projectedValue
.
Doug
Well switching the sides shouldn‘t raise any errors in that example as the most inner wrappedValue
will be Int
but the projection will be of type Binding<MyWrapped<Int>>
then.
In the previous example I would need to write _number.wrappedValue.projectedValue
to get a Binding<Int>
value.
In case of the swapped order I now need to write $number[\.wrappedValue]
or just $number.wrappedValue
due to @dynamicMemberLookup. To get String
projection I’m required to write again _number.wrappedValue.projectedValue
.
It‘s still is unclear to my why init(initialProjectedValue:)
could or should ever be a thing and why the projection is build to potentially allow that init, which is not even decided yet. The behavior of the projection must be set in stone now, it would be too late to change it later if we decide not to allow init(initialProjectedValue:)
in the future.
This makes sense, but it does raise a composition-related observation. I created an Inspectable
property wrapper that has reference semantics and conforms to BindableObject
.
The specific use case in the linked example does not require it to be used with ObjectBinding
because it performs a manual frame-based animation driven by a RenderClock
(backed by CADisplayLink
). However, in a more typical app context it would need to be composed with ObjectBinding
in order to invalidate the view when the value changes.
Unfortunately, I'm not sure the current proposal offers a way to compose these wrappers in a way that is as clean as might be desired. Users would need to compose @ObjectBinding @Inspectable
in that order and use _foo.value
to access the Inspectable
instance when injecting it into the view hierarchy. Having to use _foo.value
isn't terrible, but it probably wouldn't be obvious to most users either (at least until they learned the pattern).
I have thought about tweaking Inspectable
to project a Binding
, having users use _foo
when injecting the Inspectable
instance into the preference value instead. If you add this rule then it would not be composable with ObjectBinding
at all. In that world, supporting a Binding
projection and the ability to compose inside an ObjectBinding
would require two separate Inspectable
property wrappers. I think it would get confusing for users pretty quickly to know which wrapper to use in a specific context.
I don't have any concrete suggestions to offer, but I think this is an interesting real-world example of composition. One possibility would be to bring back $$
, but this time with consistent meaning. $$foo
would refer to the projection provided by the wrapper just inside the outer wrapper. $$$foo
would refer to the projection provided by the third layer, etc. I haven't thought through whether this is a good idea or not, but it would be one way to make all of the projections available.
From proposal.
@propertyWrapper
enum Lazy<Value> {
case uninitialized(() -> Value)
case initialized(Value)
init(initialValue: @autoclosure @escaping () -> Value) {
self = .uninitialized(initialValue)
}
var wrappedValue: Value {
mutating get {
switch self {
case .uninitialized(let initializer):
let value = initializer()
self = .initialized(value)
return value
case .initialized(let value):
return value
}
}
set {
self = .initialized(newValue)
}
}
}
later in proposal.
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)
Specifically I am confused about the above reset .reset and how does it get attached to an Int. Maybe its a typo?