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

Ah, the implementation is running behind a bit and didn't get the full name change. Use the name delegateValue rather than wrapperValue.

Doug

2 Likes

The final restriction is unnecessary; I'll drop it, thanks!

The override one doesn't make sense, though. We're still bound by the requirement that a stored property cannot be an override:

class Super {
  var foo: Int = 17
}

class Sub: Super {
  override var foo: Int  // error: cannot override with a stored property 'foo'
}

So we would have to have a getter/setter on the overriding property, which isn't allowed on a property with a wrapper. We also wouldn't want to produce a new backing storage property here, nor do we have any way to override the backing storage property from the superclass. In effect, we'd have to bend other rules to have a property wrapper on an overriding property, but we don't get anything out of it because overriding a property (without adding the $) does the same thing:

@propertyWrapper struct A<T> { var value: T }

class Super {
  @A var foo: Int
}

class Sub: Super {
  override var foo: Int {
    get { }
    set { ... }
  }
}

Doug

1 Like

Thanks! Now it's working as I thought it should. I hope this gets proposed soon.

Great to hear that it was an already lifted restriction. I think I might confused you by my c example. Of course I don't want to provide another storage in the sub-class, nor do I want to override a property of superclass with a new stored property. To me the wording of the previously quoted restriction read as they merge two things:

  • a property with a wrapper that is declared within a class must be final (and cannot be overriden from a subclass - which is an implication of final)
  • you shall add a property attribute to a an overriden property from a superclass ("and cannot override another property")

All I wanted to verify was that the example you provided with Super and Sub at the very bottom is going to be valid with the initial implementation. :slight_smile: Here I just want to inject logic into get and set of the overriden property in Sub and call super.foo to access the synthetized implementation from Super. That makes the following quoted code sample reasonable and valid from my point of view:

class Super {
  @A var foo: Int
}

class Sub: Super {
  override var foo: Int {
    // or override using `get/set` and call `super.foo`
    willSet { print(newValue) }
    didSet { print(oldValue) }
  }
}

You may want to clarify in the proposal that property wrappers can be:

  • non generic
  • generic (where zero or one of the generic types is the type of value)

I think some of the readers that did not followed all the pitch threads noticed that fact. I also might be blind but I don't see anywhere in the proposal that would describe that.

Thank you.

I wanted to ask about didSet and willSet, will they be available on properties that have a wrapper? I assumed the answer is no, but was hoping that's not the case because it would be incredibly handy on property observers that operate on variables that contain data.

The most straightforward example that jumped out at me was the UserDefault<T> example linked in the proposal, if you wanted to have an architecture where you update the data and the UI changes to reflect it. There are definitely more examples I can think of as well, and would still love to have a way to implement such code.

It should be possible to rebuild property observers with wrappers, shouldn't it?

Ah, yikes! I forgot to update the "Implementation" field (which I've done now). The implementation is on master (behind the old-named hidden attribute @_propertyDelegate), so a toolchain built from master will give you the most up-to-date implementation. I just put up a fix, thanks!

Doug

1 Like

Yes, they can have didSet / willSet. I went ahead and updated the proposal to make this explicit.

Doug

9 Likes

Do you have a particular example you'd like me to include in the proposal? I feel like we've seen abstract examples, but not concrete ones that are worth calling out in the proposal.

  • Doug

I tried adding a little more justification for wrapperValue in this new commit. Not sure what more to do to sell this aspect of the proposal :(

Doug

2 Likes

I'm wondering why this change was added. It seems at the very least we should limit property wrappers to start with a capital letter (or _, followed by a capital letter) to prevent attribute name collisions in the future (since attributes are usually spelt with a lowercase letter)

Personally, I'm partial to the more verbose @property(wrappedBy: ...) syntax (the precise label not being important), which is more easily searchable (what does a developer who has never seen the @MyCustomWrapper syntax search for to figure out what it means?). I think it would be great to at least add this to "Alternatives Considered".

Thanks for continuing to try and flesh it out more. Like I said, I trust the core team to make the right call on this aspect of the proposal.

The example in the new commit can also be implemented using an initializer extension on UnsafeMutablePointer:

extension UnsafeMutablePointer {
  init(arena: StorageArena, initialValue: Pointee) {
	    self = arena.allocate(Value.self)
	    self.initialize(to: initialValue)
   }
}

With this approach you would have the same advantage of switching the backing allocator without impacting clients of someValue just by using a different initializer. Are there reasons you feel that a design using new wrapper types with wrapperValue is better than the initializer extensions on UnsafeMutablePointer? Adding some elaboration along these lines would explain why you feel the feature adds value in these specific use cases.

This restriction got a bunch of push-back during the first review because we don't have casing-like restrictions anywhere else in the language.

Doug

1 Like

I have a home-grown Observable implementation (for use where I don't want to drag in something heavy like RxSwift), and I am experimenting with using property wrappers to provide a stateful observer (like the RxSwift example you mention in the pitch). Because it's wrapping a preexisting class, I find that wrapperValue is perfect for giving access to the Observable API without writing a few dozen method forwarders in the property wrapper.

Example
struct StatefulWrapper<V> {
    var value: V {
        didSet { observer.next(value) }
    }

    private let observer = Observer<V>()

    var delegateValue: Observer<V> { return observer }

    init(initialValue: V) {
        value = initialValue
        observer.next(initialValue)
    }
}

class C {
    let lb = LinkBag()

    @StatefulWrapper var a: Int = 0
    @StatefulWrapper var b: String = ""

    init() {
        a = 4
        $a.on(next: { print($0) }).add(to: lb)
        $b.on(next: { print($0) }).add(to: lb)
        $b.map({ $0.uppercased() }).on(next: { print($0) }).add(to: lb)
    }
}
3 Likes

Hmm. I had somehow gotten the impression that generic parameter inference for wrapper types was implemented by unifying the declared value type of the storage with the type of the value property, i.e.

@propertyWrapper
struct Function<T, U> {
  var value: (T) -> U? { ... }
}

 ...

@Function var f: (Int) -> Float?   // infers T=Int, U=Float

But it looks like the actual proposal just drops in the value type as the generic argument when the wrapper type has a single type parameter, then validates that the types are equal. The unification approach seems preferable in a number of ways; e.g. it allow structural constraints to be naturally imposed on the value type (e.g. "must be an optional type"), and it generalizes better to compositions (i.e. you start by inferring parameters for the innermost type, then use that as the value type for the next wrapper type).

15 Likes

I love the new proposal a lot. Thank you for diligently and stubbornly revising, its polish really shows. :blush:

Minor nit. For the changes around allowing the language, the proposal has this diagnostic:

error: cannot declare entity with $-prefixed name '$y'

I’d really like to see that diagnostic clearly reflect the rule that’s being created here, whatever that rule ends up being. “Only the compiler can create names prefixed with $,” or something. It’d really help with learning this in the wild. (“The compiler does this” is why I liked reusing #foo in the previous thread, but if the Core Team is happy with $, I’m happy with it.)

Something between a question and a crackpot theory. I noticed in the May 29 snapshot that key-paths can apply to both a wrapped property foo and its $foo. Is this relationship formal and intended? (I really wasn’t sure because interfacing with $foo at all appears quite crashy in this snapshot.) If so, it should be mentioned in the proposal body.

I was slightly surprised that \.foo pointed at the wrapped type instead of the wrapper. I don’t really know what I was expecting. How it behaves right now is entirely natural; I guess I had the Codable section of the proposal in mind. I was noodling on using key paths as a cheat to avoid solving for the injecting-self problem, but didn’t make much progress because of the crashing.

Either way, I’d like to see that bit formalized. I know the proposal is a small novel by now, I hope it doesn’t need to get much bigger. Thanks again!

1 Like

Even with a single generic type parameter, it does not mean that it must be the type of value. The following wrapper is also valid:

@propertyWrapper
struct GenericIntWrapper<NotValue> {
  var value: Int
  var notValue: NotValue
}

Because of the generalized design I pointed out in the previous discussion regarding composition that theoretically we could make the compiler understand a single and explicit nested property wrapper, because the compiler only needs to traverse nested value to match the type, but does not have to infer it anymore:

@C<B<IntWrapper>, String> var num: Int

This is a reduced composition of wrapper attributes in cases the multiple attribute composition would be more verbose, otherwise you can use the multiple attribute form with type inference that should be more compact.

Yes, I understand that the proposal doesn't attempt to infer type parameters if they're given explicitly.

I think the proposal's approach for composition makes more sense to me than automatically traversing value properties until you get to something of the right type for the declaration.

1 Like

Sure, I don‘t have anything against the proposed approach. All I was trying to say was that we 'could' extend it a little to make the compiler understand a single wrapper attribute that is nested using other wrapper types, which will be more compact only in a few cases where type inference is not possible and requires the developer to provide the generic type parameter list explicitly anyway. Other than that the proposals way is preferred. View is like a special convenient, but nothing else. :)

@Douglas_Gregor there is one more thing that I would like ask.

Technically we‘d always have a pair of properties, one stored ($ prefixed) and one computed (with synthetized accessors).

Does it mean that the access exposure is also independent?

private public(wrapper) ...

I would argue that this is different from something like private public(set), because the second access modifier refers to a standalone stored property.