The compiler needs to be able to evaluate generic arguments at compile-time. There are plenty of use cases for @Lazy
that do not use compile-time constant expressions.
Note that non-zero cost solution can be implemented today, without any new compiler features:
private class Box< Value: Hashable> {
let value: Value
}
// TODO: weak reference set
private var boxes: [AnyHashable: AnyObject] = [:]
private func getBox<Value: Hashable>(value: Value) -> Box<Value> {
if let b = boxes[value] { return b as! Box<Value> }
let b = Box<Value>(value: value)
boxes[value] = b
return b
}
@propertyWrapper
struct Shared<Value: Hashable> {
private box: Box<Value>
init(value: Value) {
self.box = getBox(value: value)
}
var wrappedValue {
get { box.value }
set { box = getBox(value: newValue)
}
}
extension ClosedRange {
func clamp(_ value: Bound) -> Bound {}
}
@propertyWrapper
struct Clamped<Value: Comparable> {
@Shared var range: ClosedRange<Value>
private var value: Value
init(wrappedValue: Value, range: RangeStorage<Value>) {
self.value = range.clamp(wrappedValue)
self.range = range
}
var wrappedValue: Value {
get { value }
set {
value = range.clamp(newValue)
}
}
}
@shared
is being proposed for property wrappers so extensions are out of scope.
The goal of this feature is exactly to optimize wrapper storage, not to come up with a way to implement a non-zero cost alternative.
Thanks for the input! Other folks (@hisekaldma and @benrimmington) have also pointed that this feature would be desirable as implementation detail. There were great suggestions in the thread that would help get the feature closer to this direction.
One of the main questions I had before this thread was about the "role" of the storage type. I will take these feedbacks in and iterate on the solution!
Oh, I was actually able to dig up @Joe_Groff‘s old tweet regarding this idea:
I have some remarks and questions:
-
I am not sure that the problem this pitch is trying to solve warrants adding an extra feature for it. And I definitely don't think that solving this problem warrants adding three new pieces of syntax (
@shared
before let,@shared
before initializer arguments andsomeProperty$shared
). -
I also think that users of a property wrapper should not have to be aware if it uses shared storage or not.
-
Would the current form of this proposal allow for the creation of an
@Lazy
property wrapper with shared storage where the closure passed to the property wrapper captures other properties of the class? For example:
class TimesTwo {
var input: Double
@Lazy({ return 2 * input }) var output: Double
...
}
- To me, the
@shared let
looks a lot like the@State
in SwiftUI. Let me compare:
SwiftUI | Property wrappers |
---|---|
@State |
@share |
View structs that share the same place in the UI also share the same state. | Property wrapper instances that share the same use site in code also the same share state. |
Maybe I miss the point, but couldn't the same usage pattern be used in this case (I am using @State
here for the lack of a better name):
@propertyWrapper
struct Clamped<Value: Comparable> {
@State var storage: RangeStorage<Value>
private var value: Value
init(wrappedValue: Value, range: ClosedRange<Value>) {
_storage = State(initialValue: RangeStorage(range))
value = storage.clamp(wrappedValue)
}
var wrappedValue: Value {
get { value }
set {
value = storage.clamp(newValue)
}
}
}
I think I would find it massively problematic if property wrappers had a capability that you couldn't achieve with a "regular" type wrapper, beyond the @/$ syntax features of property wrappers. (Honestly maybe they already do have capabilities that you can't achieve with a type wrapper, I haven't been keeping up with all of the property wrapper related proposals).
In other words, if this feature is useful for property wrappers, then surely it's useful for a generic type wrapper too that doesn't have the syntactic features of property wrappers. That should not be relegated to a future direction.
This example ought to be just as valid as the motivating example:
struct Clamped<Value: Comparable> {
shared let storage: RangeStorage<Value>
var wrappedValue: Value { ... }
// a plain struct, with properties that will be shared across instances.
struct RangeStorage<Value: Comparable> { ... }
}
struct Hud {
var value = Clamped(storage: RangeStorage(0...14), wrappedValue: 5)
}
This looks exciting, thanks for your efforts!
I agree with some of the comments made and am concerned that the proposed syntax would push implementation details (i.e. knowledge of what is/isn't shared) to consumers. The syntax provided in the document you linked looks pretty simple by comparison and doesn't require any new spelling. Is there a reason the proposal has moved away from that? (forgive me if you have already explained.. I did read most of the comments but I may have missed it)
Thanks again, I am quite eager to see what I can over-engineer when I can finally mix property wrappers with 'instance' properties with things like Codable

In other words, if this feature is useful for property wrappers, then surely it's useful for a generic type wrapper too that doesn't have the syntactic features of property wrappers. That should not be relegated to a future direction.
Could the OP (or anyone, really) comment on why this pitch is for property wrappers rather than types in general, as @Syre suggests?
What special need do property wrappers have that isn't shared by (say) structs that have a property that essentially parameterizes them?
As an example use-case of a slightly different kind, I sometimes add a constant let
instance property to structs even though the declaration could be static
, just because it's easier and more compact to reference the value using instance syntax. In some cases, I might want to do the same with a let
instance property that isn't always initialized to the same value.
Aside from (say) interacting with C code that needs a specific struct
layout, is there any reason why such let
properties shouldn't routinely be optimized away into shared/cache storage?

What special need do property wrappers have that isn't shared by (say) structs that have a property that essentially parameterizes them?
Part of the motivation of property wrappers is to be able to implement declaration modifiers like lazy
in libraries instead of in the compiler. This isn't possible to do with the same performance characteristics of the implementation in the compiler without this feature. Property wrappers also have an obvious per-declaration syntax that normal types don't have (perhaps yet), which is via arguments in the property wrapper attribute that is attached to a declaration.
If you have use cases for per-declaration shared storage as a general feature, please surface them! We're still figuring out the shape of the design, how general it should be, whether this approach is better than the subscript alternative, etc; that's the purpose of this thread
Right, I think the central question for people who want to be able to apply this to a broader set of types than just property wrappers is to define the scope of sharing.
The basic concept behind this feature is that some values can be divided into "value-specific" components that need to be stored inline in every value and "shared" components that can meaningfully be shared across different values. For that to work, the implementation has to be able to statically identify during initialization that specific parts of the value should be used to initialize shared storage instead of value-specific storage, it has to determine what exactly they're shared among, and it has to be able to reconstitute the combined value when the value is accessed (perhaps by either building a composite value or by passing the shared components as extra arguments).
With property wrappers, there's a property wrapper type, there's a declaration of value storage, and there are no uses of the value stored in that storage that don't understand that they're using that specific declaration. That creates a simple answer to all our problems: the shared storage is what the property wrapper says is shared, it's shared for all instances of that specific declaration, and uses of the declaration can use their knowledge of the declaration to reconstitute the value.
With an arbitrary value of some type, some key parts of that don't exist. I can certainly have the type tell me what parts of it are shared. For a specific expression that constructs a value, maybe I can pull out the shared parts and use them to initialize shared storage which will be common for all values built by that expression. But some use of that value which just knows the type isn't going to be able to know the shared storage I have in mind; I'll have to pass a reference to that shared storage along as part of the value somehow. That's already a significant compromise; in terms of the code-rewrite I described above, there's no way to use the Instance
type, just the Composite
.
And the ability to do these supposedly more general things comes at significant cost. For example, if I can construct one of these values with totally dynamic arguments, then the implementation will need to separately allocate the storage for the shared parts of the value. That implies that either the shared storage leaks permanently for such dynamically-constructed values or that the composite type has to be able to garbage-collect it, and therefore do all the bookkeeping/reference-counting which that implies, which is not necessary for the property-wrapper use case. Also, if I can propagate a value of the composite type around, and I can e.g. assign that over another value of the composite type, then maybe I can change the shared storage associated with a particular declaration. That directly undermines the storage optimization intended for the property-wrapper use case, because an overwritten property wrapper would no longer use the same shared storage as would normally be determined by the declaration. It also breaks invariants in the likely scenario that the exact shared storage for a particular declaration (e.g. an integer range or a callback function) is semantically important to its operation. (This is why semantically the composite type needs to be an unmovable type, as I suggested in my code-rewrite rule.)
I appreciate the explanation here, though admittedly I think most of it has gone over my head.

With property wrappers, there's a property wrapper type, there's a declaration of value storage, and there are no uses of the value stored in that storage that don't understand that they're using that specific declaration. That creates a simple answer to all our problems: the shared storage is what the property wrapper says is shared, it's shared for all instances of that specific declaration, and uses of the declaration can use their knowledge of the declaration to reconstitute the value.
I guess I'm struggling to identify exactly what it is about property wrappers that makes the statement above true for property wrappers, but untrue for regular types (especially because, at least as I understand it, the non-syntactic behavior of every property wrapper is currently fully expressible as a regular type, and every property wrapper can be used in their desugared form currently).
Considering the example from the detailed design section, but modified slightly:
struct Hud {
@Clamped(RangeStorage(0...14)) var bar = 5
var foo = Clamped(wrappedValue: 5, @shared: RangeStorage(1...9))
}
// desugars to
struct Hud {
static let bar$shared = RangeStorage(0...14)
static let foo$shared = RangeStorage(1...9)
var bar = Clamped(wrappedValue: 5, @shared: bar$shared)
var foo = Clamped(wrappedValue: 1, @shared: foo$shared)
}
Is that "desugars to" comment still correct for both bar and foo here?
If the "normal" syntax declaration of foo still desugars here, why wouldn't foo still desugar if Clamped was not a property wrapper?
If the "normal" syntax declaration of foo does not desugar here, is there something specifically about bar being declared using property wrapper syntax that enabled the desugaring?

I guess I'm struggling to identify exactly what it is about property wrappers that makes the statement above true for property wrappers, but untrue for regular types
A normal value can come from anywhere: it can be stored in a property, yes, but it can also be returned from a function, passed as an argument to a function, directly constructed by a call to an initializer, etc. By design, when you're using the value, the only extra information you have besides the value itself is its static type. So yeah, maybe all the values constructed by a particular initializer call have some data in common that could be shared between them; but when you're using that value later, you don't know what initializer call was originally used to construct it, so the only way you can get at that shared data is if it's passed along with the value itself. We could maybe optimize the representation so that it passes the shared data along more efficiently, but we can't completely elide it.
In contrast, we know exactly how a property wrapper is constructed, and we know when we're using it as an "independent" value, and the wrapper itself isn't normally passed around separately (so we could potentially forbid that for a particular wrapper if it was advantageous). So we do have an opportunity to split off the shared data and not store it in the value that's stored inline in the property / local frame / whatever. And that gets property wrappers much closer to parity with built-in modifiers like lazy
.

In contrast, we know exactly how a property wrapper is constructed, and we know when we're using it as an "independent" value, and the wrapper itself isn't normally passed around separately (so we could potentially forbid that for a particular wrapper if it was advantageous).
Property wrappers also have an opportunity to construct a different, self-contained value to be passed around externally via the projected value. I think it's reasonable to to forbid passing the optimized wrapper storage around, and suggest using projected value for this purpose instead.
I do think there's a way to extend the idea of shared storage to types more broadly, but I'm concerned that it has sufficiently different trade-offs that it might compromise the feature's usefulness for property wrappers.
If you think of shared storage as extending from a particular construction site (e.g. MyType(x: 1, y: 2, z: 3)
), and we know that MyType
uses shared storage, then we could validate and construct the shared storage from the shared arguments to that specific initializer-call expression in basically the same way that I described it above for property wrappers:
struct MyType {
@shared var x: Int, y: Int
var z: Int
init(@shared x: Int, @shared y: Int, z: Int) { ... }
@shared init(x: Int, y: Int) { ... }
}
MyType(x: 1, y: 2, z: 3)
// rewritten to
static let anonymous_shared = MyType.Shared(x: 1, y: 2)
MyType(&anonymous_shared, z: 3)
MyType
is then rewritten to carry a pointer to a MyType.Shared
, so that you get useful sharing between all the values constructed by a specific expression, but the value can still be used in a normal, first-class way. This doesn't completely eliminate the overhead associated with passing around the shared data, but it may significantly lower it if the shared data is large or expensive to copy. It would be very invasive for programmers to make this sort of change manually.
The thing is, I don't know how to efficiently combine that with what we want to do with property wrappers. The natural representation for MyType
is something like the following; notice how all the non-shared properties are stored inline in the struct:
struct MyType {
struct Shared {
var x: Int, y: Int
}
var shared: UnsafePointer<Shared>
var z: Int
}
But for property wrappers, we really want to be able to store all the non-shared properties somewhere by themselves and then reconstitute a value of MyType
from a reference to that without having to copy any of the non-shared data. For MyType
, which just has a single Int
of non-shared storage, it wouldn't be a big overhead to copy the Int
into the reconstituted value (and then copy it back if we needed the MyType
for a potentially-mutating operation), but for an arbitrary type that could be a big deal. This gets into the conversation that @Joe_Groff and I had above about in-place access; he's absolutely right that if we need to copy the non-shared storage back and forth, we may have a hard time eliminating those costs. So for property wrappers we really want a representation more like what I had in my earlier posts:
struct MyType {
struct Shared {
var x: Int, y: Int
}
struct Instance {
var z: Int
}
var shared: UnsafePointer<Shared>
var instance: UnsafeMutablePointer<Instance>
}
or at least something like the following, which would allow values to be passed around in a first-class way, but also allow efficient reconstitution as long as they weren't:
struct MyType {
struct Shared {
var x: Int, y: Int
}
struct Instance {
var z: Int
}
enum InstanceStorage {
case inline(Instance)
case separate(UnsafeMutablePointer<Instance>)
}
var shared: UnsafePointer<Shared>
var instance: InstanceStorage // when the value is copied, this always transitions to the inline case
}
But this seems like a substantial burden to impose on the property wrapper use case.
Looks like Isa fixed this typo already (the updated draft is on GitHub)
The initialization of the storage value itself follows the same principles as static variables: it can't use instance variables or methods that depend on
self
being initialized.
What about this scheme for lowering?
extension ClosedRange {
func clamp(_ value: Bound) -> Bound {
return Swift.min(Swift.max(value, self.lowerBound), self.upperBound)
}
}
protocol ClampedShared {
associatedtype Value: Comparable
static var bounds: ClosedRange<Value> { get }
}
@propertyWrapper
struct Clamped<Value, Shared: ClampedShared> where Shared.Value == Value {
private var value: Value
init(wrappedValue: Value) {
self.value = Shared.bounds.clamp(wrappedValue)
}
var wrappedValue: Value {
get { value }
set {
value = Shared.bounds.clamp(newValue)
}
}
}
private enum Hud_bar_Clamped_Shared: ClampedShared {
static var bounds: ClosedRange<Int> { 0...14 }
}
private enum Hud_foo_Clamped_Shared: ClampedShared {
static var bounds: ClosedRange<Int> { 1...9 }
}
struct Hud {
@Clamped<Int, Hud_bar_Clamped_Shared> var bar = 5
@Clamped<Int, Hud_foo_Clamped_Shared> var foo = 1
}
My thinking behind this goes as following:
Currently in Swift everything related to a nominal type and shared across instances is associated with a type. I was thinking how shared storage could keep this as is and avoid introducing new kind of sharing.
Applying this to the example of Clamped
means that it should be lowered into the generic type, with extra generic argument for shared data. We don't have values as generic parameters yet, and I'm assuming we won't get them soon, so some sort of poor-man's solution is needed.
To pass values as a generic parameter, as a workaround, we can wrap the value into a type conforming to a protocol with static requirements. This allows simple mapping from types to values. It is possible to come up with a scheme mapping values back to types, preserving equality, but as far as I can see, it is not needed for this proposal.
This lowering scheme organically answers how to handle values of types with shared values outside the context of property wrappers - they are just passed around as generic values:
func inc<Shared: ClampedShared>(value: inout Clamped<Int, Shared>) {
value.wrappedValue += 1
}
Using generics it is possible to express requirement that different values must be coming from the declaration of the same property:
func transfer<Shared: ClampedShared>(from: inout Clamped<Int, Shared>, to: inout Clamped<Int, Shared>, amount: Int) {
from.wrappedValue -= amount
to.wrappedValue += amount
}
var h1: Hud = ...
var h2: Hud = ...
transfer(from: &h1.foo, to: &h2.foo, amount: 1) // OK
transfer(from: &h2.bar, to: &h1.bar, amount: 1) // OK
transfer(from: &h1.foo, to: &h2.bar, amount: 1) // Error

and the wrapper itself isn't normally passed around separately (so we could potentially forbid that for a particular wrapper if it was advantageous)
Ok I think I get what you are saying now! I think you are saying that this particular storage sharing optimization relies on the "typical" use of property wrappers, which is to say that usually you are never going to touch the wrapper type outside of the property declaration site.
And now it seems that you are proposing a compile-time enforcement of this "typical" use of property wrappers when they utilize this storage sharing optimization, I think that makes sense so that one doesn't accidentally escape this optimization without realizing it.
Please correct me if this understanding isn't quite right!

I guess I'm struggling to identify exactly what it is about property wrappers that makes the statement above true for property wrappers, but untrue for regular types (especially because, at least as I understand it, the non-syntactic behavior of every property wrapper is currently fully expressible as a regular type, and every property wrapper can be used in their desugared form currently).
For a normal type, there are two "interesting" places to store information:
- In each instance of the type (instance properties).
- Shared by all instances of the type (type properties).
For a property wrapper, there is a third "interesting" place to store information:
- Shared by all instances of the type that are wrapping the same property.
I suppose that in theory, there might be a case for a more general mechanism to share variables between a subset of instances of a type, but…how would you actually do that? How would you describe which instances should be grouped together? How would a user initialize these variables?

For a property wrapper, there is a third "interesting" place to store information:
- Shared by all instances of the type that are wrapping the same property.
I think this third “interesting” place to store information can be generalized as:
- Shared by all instances of the type that can be linked to the same property.
I think this is possible (with the right set of restrictions), but I will refrain from providing further detail about how I think this can work until I can come up with a concrete example of where I think it could be useful.
How does this play with Decodable
?
Decodable
requires init(from decoder: Decoder) throws
but this proposal requires an additional @shared storage
parameter.
Shared storage would be great to implement something like DefaultCodable
from the BetterCodable package without the need to define a custom type for each default value. Because you currently need to define a custom type for each default value, the BetterCodable package includes a lot of different property wrappers for the most common default values like empty array, empty dictionary, false and so on. With shared storage and proper support for Decodable
this could be greatly simplified.