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

Right, but you're still copying the Observable by reference and the value by value. As far as I can tell the issue I'm describing would be reproducible with your library and StatefulWrapper.

Are we still considering global and local variables in the first version of this feature? The proposal explicitly mentions them, but actually trying to use them (in the latest snapshot from May 29th) on a global causes interesting missing symbol errors, while locals just say they aren't implemented yet.

If there wasn't a delegateValue (current snapshot), then c.$a would return the wrapper, and that would cause a copy to be made. However, with the pass-through, the Observer is returned, and that's a reference.

Right, that’s why this makes a good motivating example. Direct use of StatefulWrapper has sharp edges that are removed by wrapperValue.

This motivating example suggests that it might be good if a property wrapper type could ban direct usage and force users to use it as a wrapper. It might make sense to do that for all types that use wrapperValue or it might make sense for it to be opt-in. Either way, it would be nice to close off all avenues to improper use of a type like StatefulWrapper. Any thoughts on this @Douglas_Gregor?

I had intended to document the unification approach, but I apparently I didn't do so. I've now updated the proposal to use unification between the type annotation on the original property and the value property's type, including your example. This also addresses @DevAndArtist's desire to have non-generic and multiple-generic-parameter wrapper types as examples in the proposal. Thanks!

Doug

6 Likes

I have the following trivial example to show composition:

struct Adjusting<V> {
    var value: V {
        didSet { value = transform(value) }
    }

    private var transform: (V) -> (V)

    init(initialValue: V, _ transform: @escaping (V) -> (V)) {
        self.value = initialValue
        self.transform = transform
    }
}

@_propertyDelegate
struct Clamping<V: Comparable> {
    @Adjusting var value: V

    init(initialValue: V, min minimum: V, max maximum: V) {
        $value = Adjusting(initialValue: initialValue,
                           { max(min($0, maximum), minimum) })
    }
}

@_propertyDelegate
struct Min<V: FixedWidthInteger> {
    @Clamping var value: V

    init(initialValue: V, min minimum: V) {
        $value = Clamping(initialValue: initialValue,
                          min: minimum, max: V.self.max)
    }
}

@_propertyDelegate
struct Max<V: FixedWidthInteger> {
    @Clamping var value: V

    init(initialValue: V, max maximum: V) {
        $value = Clamping(initialValue: initialValue,
                          min: V.self.min, max: maximum)
    }
}

class C {
    @Adjusting(initialValue: 0, { max(0, $0) }) var a: Int
    @Clamping(initialValue: 1, min: 1, max: 7) var b: Int
    @Min(initialValue: 0, min: 7) var c: Int
}

Is there any way to change the syntax so that it's not necessary to provide an initialValue to the constructors? I'd much rather see something like this:

@Clamping(min: 1, max: 7) var a = 13 

rewritten by the compiler as

let $a = Clamping(initialValue: 13, min: 1, max: 7)
var a: Int {
    get { $a.value }
    set { $a.value = newValue }
}

Essentially extend the current logic which looks for init(initialValue: T) to also look for init(initialValue: T, ... <other args from declaration>)

6 Likes

Here's another one that I think should work:


struct Color {
    @Clamping(initialValue: 0, min: 0, max: 255) var r: Int
    @Clamping(initialValue: 0, min: 0, max: 255) var g: Int
    @Clamping(initialValue: 0, min: 0, max: 255) var b: Int
    @Clamping(initialValue: 0, min: 0, max: 255) var a: Int

//    init(r: Int, g: Int, b: Int, a: Int) {
//        self.r = r
//        self.g = g
//        self.b = b
//        self.a = a
    }
}

var c = Color(r: 1, g: 1, b: 1, a: 255)

Without the init(), the compiler gives following error:

Cannot convert value of type 'Int' to expected argument type 'Clamping<Int>'

Should not the types of the properties be Int?

3 Likes

FYI, a couple typos in Restrictions: "may not declared"

Anyway Love the new name. It instantly gives me an idea of what it's for (and spawns loads of ideas).

I think I'm all for this. I like the backing storage being private and the nesting makes sense.

My only question is for:

A property with a wrapper that is declared within a class cannot override another property.

  1. Why not? Is this a technical limitation, or a design choice?
  2. Can the wrapped-property be overridden in a subclass?
1 Like
  1. This is just logical, as the subclass should not reintroduce a new stored property and override a stored property from a superclass pointing that that.
  1. Yes, this restriction was lifted (the proposal required wrapped properties to be final on classes before).

Why is that always logical? I can see it being a problem when doing things around memory management or initialization semantics. However generally this is already possible, it just requires more code.

e.g. given something like this:

class Parent: Codable {
    var property: String?
    init(property: String?) {
        self.property = property
    }
}

class Child: Parent {
    var _property: String?
    override var property: String? {
        get {
            if let value = _property {
                return value
            }
            else {
                return "hi"
            }
        }
        set {
            _property = newValue
        }
    }
}

You could instead theoretically do:

class Child: Parent {
    @BackupValue("hi")
    override var property: String?
}

As long as it fulfills the contract of the Property, why prevent it?

Why would you want to fake the original storage and create a second stored property and waste memory? (Nothing sarcastic in this question.)

If we'd allow this, you could write code as follows, but why would you want that?

class A {
  @IntWrapper(initialValue: 1) var number: Int

  func numberFromA() -> Int {
    return $number.value
  }
}

class B: A {
  @IntWrapper(initialValue: 2) override var number: Int

  func numberFromB() -> Int {
    return $number.value
  }
}

let b = B()
b.numberFromA() // 1
b.numberFromB() // 2

There you're overriding a wrapped property, which will be allowed under the proposal, so I'm assuming you meant A.number to not be wrapped?

If the original storage is "faked"...it's not storage, is it? Anyway if your design is constrained by the memory of a few properties, you have bigger things to worry about.

I would think giving a backup value for an optional property is a pretty good use case. Regardless, if the argument is this should never be allowed there should be a good reason since you're constraining design possibilities.

I don't understand this. In A the compiler is told to synthetize accessors for a 'computed property' called number and generate a 'stored property' called $number, where number is linked with $number.value. There is nothing that is overriden by the meaning of the override keyword in Swift.

I'm not sure what you're trying to say here either. In your original example you create a stored property called property on the superclass. Then in the subclass you create another stored property called _property. By default property would use dynamic dispatch and write into super.property, yet you override it and re-route both accessors to the new stored property provided by the subclass. You may have a point for doing this, I don't see it. That is why I asked for clarification on why are you wasting memory and decouple the original property property from its storage?


Remember that property wrappers are private by default and the subclass B does not have any information that A.number is wrapped or not, it just knows that it's get/set and normally you'd forward the overriden accessors in the sub-class using super.number. However in your example you didn't forward anything, so I showed an example that is fairly similar to yours, which feels odd to me.

You had

class A {
  @IntWrapper(initialValue: 1) var number: Int
}

class B: A {
  @IntWrapper(initialValue: 2) override var number: Int
}

In which a wrapped property is being overriden. This will be allowed in the current proposal. What I'm talking about is:

class A {
  var number: Int
}

class B: A {
  @IntWrapper(initialValue: 2) override var number: Int
}

Which is overriding a non-wrapped property and currently will not be allowed.

The point of my code was this:

    if let value = _property {
        return value
    }
    else {
        return "hi"
     }

This means aChild.property would return "hi" in the case that it was nil.

I'm not 100% how Swift deals with the storage in this case. My intention was to have _property be the only storage and have the overridden property be calculated.

Regardless, at worse it would result in an unused String property that's always nil. Obviously that's not "good"...but far from catastrophic. Regardless, that's beside the point and IMO not a reason to disallow the possibility.

Again, those examples are equivalent from the view point of visibility. B only knows that A.number is get/set. B does not know or see that A.$number exists and that A.number is linked with A.$number.

What B.number should do to preserve a single storage, hence a single stored property, it should call super.number in its accessors. However you are suggesting that it should not, which would lift the restriction the proposal already mades.

Okay then I think we speak about different things. You want a property wrapper to add a special behavior to a computed property, while 'property wrappers' as proposed provide themselves a stored property and link a chosen non-overriding computed property against it.

If I understand you correctly now, then I think you made a mistake in your example. Ultimately you want to provide a behavior for this implementation:

class Child: Parent {
    override var property: String? {
        get { return super.property ?? "hi" }
        set { super.property = newValue }
    }
}

I don't think property wrappers are designed to be used like this, at least I don't have any idea on top of my head how a property wrapper could call super if it's not lazy and don't have any access to self, so it could call super.

I think we're talking past each other here so I'll drop it after this clarification :slightly_smiling_face:

I'm not talking about visibility I'm talking about what the proposal allows. My example was to show a basic example of how someone could want to override a non-wrapped Property with a Wrapped Property it. Not to provide all possible use cases.

My point is restricting PropertyWrappers from overriding properties in all cases should have more reasoning behind it than a tiny memory hit, which could even theoretically could be compiled away.

If you want something more compelling you could wrap a property and tie it to UserDefaults:

@BoundToUserDefault(key: "firstName")
override var firstName: String

:hushed: Side note just realized that's almost definitely how I will be dealing with UserDefaults if this is implemented!!!

extension UserDefaults {
    @BoundToUserDefault(key: "userName", defaultValue: "")
    var userName: String
}

Spoiler alert, your example won't compile due to:

  • An instance property with a wrapper may not declared inside an extension.

Agh...right...would need one without backing storage. hopefully that will be added at some point :disappointed:

Small hint, you could just add static and it should compile, because user defaults should be shared right? :smiley:

Haha Yeah...probably what I'll end up doing for simple things :innocent: . Just less generalizable :grimacing:

1 Like