By "follows the same principles as static variables" I meant the shared storage is initialized upon first access, and it will be initialized only once. The global function snippet you showed could have RangeStorage stored in a global property and so it's valid code.
Perhaps the transformation would feel more minimal if it just rewrote the original type into the composite type. That is, the original stored properties would be divided into the two fairly self-contained component types, two new stored properties would be added as references to those components, and the original stored properties would just forward to the appropriate component. Then the only really elaborate transformation is with the initializers, which I think is more intrinsic to the problem if you want to avoid ad hoc argument matching.
I might be missing something. If the storage as it appears inside the wrapped property doesnât include the shared fields, or even a reference to the shared fields, and those shared fields are introduced when we convert to the aggregate form to be able to pass around a value of the formal wrapper type, then how can we do that in-place?
Maybe I'm misunderstanding what you're going for, but it seems like there are contradictory requirements for the function-local case.
The shared storage is initialized only once.
RangeStorage would be stored in a global property.
The function could be called many times, with different arguments and from different threads, potentially simultaneously.
If the allocation for shared storage is local, not global, then you can meet requirement 1 by initializing the storage once per invocation of the function, akin to how typical local let properties are initialized. However, that breaks 2.
If the allocation for shared storage is global, not local, then you can meet requirement 2 but not 1 because every time the function is invoked, it will initialize the (same, shared) storage with certain values. Moreover, you'd need synchronization to prevent data races. So this code no longer has the semantics that it had when the range was an instance property, in the instance property case, there was no synchronization needed to access the property.
The idea of shared storage is that there is one per declaration of the wrapped property, and the storage is constant across all instances of that declaration. In other words, for each wrapper attribute, e.g. @Clamped, there is only one shared storage, and every instance of that property wrapper uses the same shared storage. For local property wrappers, shared storage arguments can't use arguments to the enclosing function.
Making the shared storage local for local property wrappers is no different from a regular stored property in the wrapper, so there's no reason to use shared storage if you want that behavior.
When code needs a value of the formal wrapper type (when evaluating a reference to a particular wrapper instance), you reconstitute it from your reference to the instance storage and your knowledge of the appropriate shared value. Now you have a self-contained value, albeit one that can't be safely moved or otherwise persisted beyond the current scope.
For example, a @Clamped(to:) wrapper might support several kinds of ranges, by using a private enum type as an implementation detail, but the wrapper's initializers would only take public range types.
@propertyWrapper
public struct Clamped<Value: Comparable> {
private enum _Limits {
case closedRange(ClosedRange<Value>)
case partialRangeFrom(PartialRangeFrom<Value>)
case partialRangeThrough(PartialRangeThrough<Value>)
}
private var _value: Value
private let _limits: _Limits // TODO: Shared storage.
public init(wrappedValue: Value, to limits: ClosedRange<Value>)
public init(wrappedValue: Value, to limits: PartialRangeFrom<Value>)
public init(wrappedValue: Value, to limits: PartialRangeThrough<Value>)
public var wrappedValue: Value { get set }
}
extension Clamped where Value: Strideable, Value.Stride: SignedInteger {
public init(wrappedValue: Value, to limits: PartialRangeUpTo<Value>)
public init(wrappedValue: Value, to limits: Range<Value>)
}
Why not support actor? Is there a special reason to ban actor?
[actor class struct enum] are four fundamental types in swift, and adding actor can support shared mutable storage with async/await access across PW instances without any data race/sync problems as mentioned in:
This seems very reminiscent of SwiftUI's magic @State property wrapper. Is that part of the intention here? Can this be employed by normal users to implement @State? That would be quite lovely if so. Even if not, this seems like a nice direction for extending property wrappers.
Thanks! I somehow missed this. It would be good to describe this exact transformation in the text too.
Ah, I assumed that it would be widely usable since itâs a declaration modifier. Maybe it would be better as an attribute (@shared) than as a declaration modifier (shared) if itâs specific to property wrappers? I donât think the language has any other declaration modifiers that have such narrow allowed usage.
I can understand this. However, I think the "enclosing instance" feature is doubly weird. Not only is it a subscript, and not only does it take the enclosing instance, but it also has to awkwardly deal with correctly controlling exclusivity while accessing through the enclosing instance, by being a static subscript, and forcing you to get back to the wrapper by applying a key path to the enclosing instance. On the other hand, a subscript that takes the shared information but doesn't otherwise need to access self can still act as an instance member and read the wrapper's storage directly.
I think I get what you're trying to do with shared, but it also seems like the behavior of shared could be built from the more fundamental explicit subscript design if it didn't exist. I wonder, what would it take for shared to itself be expressible as a property wrapper? The idea of gathering a set of properties annotated with a wrapper, and setting their actual storage aside somewhere else, is also potentially generally useful; this is what wrappers like SwiftUI's @State or swift-argument-parser's @Option varieties do manually via reflection today.
It's was not the intention but thanks for pointing it out. The storage would have to be mutable in this use case though. I need to explore this
Thanks, it makes sense. I've actually updated the pitch on GitHub after some of the feedbacks, with @shared now as a declaration attribute.
Tbh I didn't mention actors because I'm still getting used to the new concurrency-related language features. Though, I should think about it and mention it in the pitch.
Maybe Iâm just lacking enough caffeine today, but parts of this proposal are a little confusing (apologies if Iâve just missed the answers).
Firstly is the purpose of it. Unless people are writing property wrappers that are storing massive objects it feels like this is going to maybe be saving KB per app, so maybe a few MB per system. Is a complex change like this worth it for such savings? Or am I underestimating just how much memory property wrappers are using?
The second is around usage. You seem to use @shared a lot. Itâs used in the property wrapper init methods, but also when creating the wrapper (e.g. @Wrapper(@shared: SomeStorage())). Does the latter imply that I can also do @Wrapper(SomeStorage()) and get a non-shared version? If not, why does @shared need to exist at all when defining the wrapper, as surely the compiler can infer it from the initialiser declaration. After all, for the vast majority of users in the vast majority of use cases, the fact that a property wrapper uses shared storage is merely an API implementation detail they probably shouldnât need to concern themselves with. If itâs not optional it would be nice if it would never be required with the compiler handling it, so it doesnât become boilerplate.
Iâm wary of using a term that is rather generic andâmore salientlyâalready claimed in the Ownership Manifesto for another purpose, whether or not we spell it with @ here.
We already have precedent for how to spell something thatâs like a static member with a twist: a class member supports overriding in subclasses.
I think this points to a natural spelling for this feature: Just as we reuse class as a declaration modifier, we could reuse @propertyWrapper also and say that a @propertyWrapper let storage is a member called storage shared among all instances of the type containing the wrapped property.
This has the natural benefit of providing a natural answer to the question of what the feature does outside of property wrappers (which is to say, it makes it obvious that the feature is designed specifically for property wrappers).
I can definitely see the value of having shared storage between property wrappers to reduce allocation size.
HoweverâŚ
As a user of the @Clamped property wrapper, why should it be my responsibility to know how its storing information? The storage mechanism for a property wrapper is an implementation detail. If I'm using this, I don't care how or where the range is stored. I care that if I try to set bar = 15, the value gets clamped to 14. Anything more than that is burdening me with unnecessary ceremony that gets in the way of me writing my app (or whatever it is I'm creating).
So, is there a way we can provide property wrapper authors with shared storage, without having to force it on clients?
I didnât read the whole post (sorry!) but I realize extensions share a similar problem.
Would it be possible for this solution to allow for something like this?
extension UIImage {
// Find a cached thumbnail or create one if necessary.
// Cache would be stored using âsharedâ storage
var cachedThumbail : UIImage { ⌠}
}
I also feel that suggested approach is a poor-manâs approach in the absence of the generic value parameters.
Could you please provide some examples of values which are valid for shared storage, but not valid for generic value parameters?
Swift is already able to generate generic types in runtime. I think it should be possible to generate generic types with arbitrary runtime value as a parameter:
protocol P {}
// Only Hashable types are allowed!
struct K: Hashable { ⌠}
struct S<let n: [K]>: P {}
func makeS() -> P {
let k = arc4random() & 1 == 0 ? [] : [.foo, .bar]
return S<k>()
}
One example is implementing @Lazy, which is typically initialized with a closure to compute the value upon first use of the wrapped value getter. Each instance of lazy needs to store this closure, even though the closure will never change across instances for a given application of @Lazy.
Conceptually this should also work, subject to availability of Equality of functions. One of the workarounds discussed in that thread was to treat closure literal as a syntax sugar for compiler generated struct in certain contexts. With this workaround in mind, it could look like this:
struct Lazy<Value, Thunk: Function0, let thunk: @autoclosure Thunk> where Thunk.ResultType == Value {
var _value: Value?
var wrappedValue: Value {
get {
if let v = _value { return v }
let v = thunk()
_value = v
}
set {
_value = newValue
}
}
}
func makeInitialValue(arg: Int) -> Bla { ... }
struct Usage {
@Lazy<Bla, _, makeInitialValue(arg: 42)> var prop: Bla
}