[Pitch #3] Property wrappers (formerly known as Property Delegates)

I've added a tiny bit more---the actual wrapper types would have different kinds of storage from which they derive the UnsafeMutablePointer, and in theory could be doing something interesting when producing that UnsafeMutablePointer. They could also be get-only, so you can't change the UnsafeMutablePointer by writing to $foo.

Doug

Looks good. Between what you have written and the example from @Avi upthread my confidence has increased. Now I just wish there was a way to prevent a wrapper type from being initialized directly in a non-wrapper context. :wink:

From the proposal:

The property declaration

@Lazy var foo = 1738

translates to:

var $foo: Lazy<Int> = Lazy<Int>(initialValue: 1738)
var foo: Int {
  get { return $foo.value }
  set { $foo.value = newValue }
}

My Example:

class Parent {
    var property: String? = "test"
    init(property: String?) {
        self.property = property
    }
}
class Child: Parent {
    var _property: String?
    override var property: String? {
        get { return _property }
        set { _property = newValue }
    }
}

This compiles and seems to work in the Swift 5 version of Playgrounds, so I would think a wrapped property would work the same with the current rules (assuming this hasn't changed in Swift 5.1).

I'm not familiar enough with how the compiler and error checking would handle this since the implementation is synthesized, but it seems like it could be made technically possible. If the rational behind the prohibition of overriding stored properties, (which I'm not familiar with), is violated by this I can understand it being a necessary artificial constraint.

If it's a current technical/scope/time constraint it can probably be dealt with later. Regardless, can't wait to try it out! :smiling_face:

Great point! I'm going to go with

error: cannot declare entity named $foo; the '$' prefix is reserved for implicitly-synthesized declarations

which is implemented here.

Yes, this is intended. foo and $foo are both properties, and either of them can be referenced by a key path. I'm not sure what to formalize here, because I feel like this is what falls out of the model.

The crashiness with accessing $foo should be addressed by this pull request. It seemed to be specific to classes.

Doug

3 Likes

I believe that the spirit of the restriction is that when you're doing an override, you generally should be making some use of the superclass's implementation of the getter/setter in some manner. If you're overriding with a stored property (whether directly or via a property wrapper), the compiler is implicitly ignoring the superclass's implementation of the getter/setter.

This is a philosophical limitation---Swift is intentionally forcing you to write more verbose code to do something your superclass wouldn't expect---but I think that's a good thing.

Doug

5 Likes

What if you make it a static variable?

Wasting memory when overriding seems such a minor concern when property wrappers are wasteful of memory in many of the proposed usages. I'm thinking of UserDefault (storing the key and default value), Clamped (storing min and max), and Lazy (storing the closure). Pretty much always those values are going to be compile-time constants, but will need to be stored within the stored property as part of the wrapper.

If those values could be generic parameters it would make much more sense I think. But you can only use types with generics right now, so it'd be cumbersome to implement and use a property wrapper made that way (you'd need one type per value :roll_eyes:).

1 Like

What if you scroll down two more posts? :partying_face:

That is excellent, thanks!

That makes total sense to me now that I’ve thought about it, and I’ll play with it more on the next snapshot. Granted, “fits like a glove into existing language features” never hurts during a proposal review. :)

This won't necessarily always be true.

The quote is Chris Lattner from the mailing list.

Eliding a compile-time constant member is rather straightforward for members of a class that are declared let and have an initializer that can be determined to have no side effect. It seems far fetched it can ever work for let values within a stored struct that are initialized from inside the wrapper struct initializer, even though the initializer is fed with a compile-time constant argument. Even if you somehow pull it out, in the presence of ABI boundaries it definitely can't work inside a @frozen enclosing struct, and it'd require a frozen wrapper type with an inlinable wrapper initializer (so the optimizer can change the memory layout depending on what the wrapper initializer is doing).

While the compiler might perhaps eventually gain the ability to do something like this, it doesn't seem to me like it could apply reliably enough to be dependable.

I’m trying to wrap my head around this, and maybe it’s because I need to read/see more examples, but at what point while working in swift would you think “a property wrapper would be perfect for this”?

Or is this feature not going to be that common?

Also, does this improve IBOutlets in anyway?

TLDR: I am trying to determine when it becomes obvious that creating a property wrapper would be the feature to use.

(Also with my currently limited understanding, property behaviors still sounds like a more descriptive/ accurate description of this)

1 Like

Not immediately but it would be great to remove that attribute from the language and move it into a library. IBOutlet could function similarly to DelayedMutable property wrapper while eliminating the requirement that an outlet must be an optional which would replace all the ugly IUO's out there.

After rereading the linked section, I understand the current design. I have to say that I don't like it. If wrapped properties are synthesized as

var wrapped: T { 
    get { $wrapped.value }
    set { $wrapped.value = newValue }
}

then conceptually, the type of the property is always T. As a user of the feature, it's entirely unintuitive to have the synthesized init use a different type for its parameters. Is there a reason it has to be this way? I didn't see a rationale for this in the pitch.

1 Like

Am I misunderstanding something, or is the Copy-on-Write example actually an implementation of Copy-on-Read?

The copy appears to happen in a mutating get:

  var wrapperValue: Value {
    mutating get {
      if !isKnownUniquelyReferenced(&value) {
        value = value.copy()
      }
      return value
    }
    set {
      value = newValue
    }
  }
let buffer = MyStorageBuffer()

@CopyOnWrite var storage: MyStorageBuffer = buffer

// There are now two references: `buffer` and `$storage.value`. Assuming
// `count` is a non-mutating getter, this should not copy the storage
// buffer. But, it actually does copy it, right?
$storage.count
1 Like

I'm very positive about this feature, but the motivation for wrapperValue is still not clear to me.

Is there any functional difference compared to writing .wrapperValue at the use site? (Other than hiding the property wrapper's API, which seems like it should be done via the standard access controls.)


Here are some of my thoughts on the motivating examples from the proposal, and from this thread.

Copy-on-write

As mentioned in my previous comment, this one seems to implement copy-on-read, so I'm not going to comment further here.

Ref / Box

If Box conformed to dynamic member lookup in exactly the same way that Ref does, wouldn't the code work exactly the same, without the need for the wrapperValue magic?

Delegating access to the storage property

The main motivation here seems to be that the implementation can be changed without affecting call site code. But, isn't that what protocols are for? If we introduce a PointerStorage protocol, then we can get the same resilience at the call site:

protocol PointerStorage {
    associatedtype Value
    var pointer: UnsafeMutablePointer<Value> { get }
}

@propertyWrapper
struct LongTermStorage<Value>: PointerStorage {
    // unchanged from the proposal, except without the
    // `wrapperValue` property.
}

@propertyWrapper
struct ArenaStorage<Value>: PointerStorage {
    // unchanged from the proposal, except without the
    // `wrapperValue` property.
}

// and in use...
// The only change here is the addition of the explicit `.pointer` reference.

@LongTermStorage(manager: manager, initialValue: "Hello")
var someValue: String

// $someValue accesses the wrapper instance, which is a `PointerStorage<String>`
let world = someValue.pointer.move()   // take value directly from the storage
someValue.pointer.initialize(to: "New value")

Avi's Observable

The concern here appears to be that a property wrapper and its stored properties can have different semantics (value vs reference).

This is a valid concern, but it applies whether StatefulWrapper is a property wrapper, or just a regular struct. I think the correct way to do this would be to have StatefulWrapper and Observer either both be classes, or both be structs.

If they are both classes, then taking a new reference to the wrapper would create a reference:

@propertyDelegate class StatefulWrapper { ... }
class Observer { ... }

@StatefulWrapper var a: Int = 0

let b = $a

// both of these notify the same set of observers
a = 1
b = 2

If they're both structs, taking a new reference to the wrapper would create a separate (observable) property.

@propertyDelegate struct StatefulWrapper { ... }
struct Observer { ... }

@StatefulWrapper var a: Int = 0

let b = $a

// Each of these notifies a separate set of observers
a = 1
b = 2

It's actually coincidental that StatefulWrapper is a good example for how wrapperValue can allow a struct to wrap a reference. That's not at all what I had in mind when I wrote it. What I was after is a clean way to expose the API of Observer without writing a few dozen forwarding functions in the wrapper. In terms of ergonomics, that remains the primary motivator for me.

Yeah, if there's no init(initialValue:), then there's no way to create a Wrapper<T> from a T, and the point of the memberwise initializer is to initialize the stored properties.

Doug

When using @CopyOnWrite var foo: T, use foo for reading $foo for read/write.

The main difference is that with @Box var foo: T, $foo itself has type Ref<T> rather than type Box<T>. Box<T> could also support dynamic member lookup, I suppose, but then $foo would be Box<T> and $foo.bar would be Ref<U>, which is... not as nice.

(I think you meant $someValue in both lines above)

I don't feel like the generics you've applied here are really changing things. The difference is at the use site---instead of having $someValue refer to the pointer that we want, you're applying the convention to use $someValue.pointer. That's possible; my view is that the design is improved by having $someValue be the pointer that we want.

Doug

The example in the pitch shows two of the parameters having the type of the property, and not the wrapper. I presume it's because those properties are known to have init(initialValue:) implementations. The first because it's initialized in the declaration, and the second because the wrapper is specified without any parameters.

If my request above, for extending the lookup logic for init(initialValue: T, ...) is implemented, would that change my Color example such that the synthesized init would accept Int parameters?

1 Like