[Pitch #2] Property Delegates by Custom Attributes

Hi all,

The first property delegates pitch has been really interesting and productive. Based on the feedback there, I've significantly revised the proposal to be based on custom attributes, eliminating the not-universally-loved by syntax. I've also tried to incorporate much of the feedback and many of the examples that came up in that thread. I'm sure I didn't capture everything, so thanks for your patience.

The proposal is different enough that I feel it's better to create a new pitch thread, so the 250+ messages from the old pitch thread don't confuse the issues. The latest proposal is here: https://github.com/DougGregor/swift-evolution/blob/property-delegates/proposals/NNNN-property-delegates.md

Thanks,
Doug

33 Likes

I know that I'm late to this game but where did the convention of camel case name of attribute come from? I prefer the @PropertyDelegate to the @propertyDelegate here.

Sorry for the bike shedding here. I'm reading your updated proposal now :slight_smile:

4 Likes

Wow, I’m really in favour of this proposal in its current form. +1 from me!

Just to clarify a few things:

  • The restriction for a property delegate type to have a single generic type parameter is now lifted? (Edit: I checked the tests in the new implementation and can confirm this restriction is gone. :tada:)

    And thank you so much for allowing explicitly providing generic type parameter list on the property delegate (@Lazy<Int> var property: Int) - I assume that will also play nicely in a generic context.

    (Edit: You replied in the other thread, but I didn't get a notification.)

  • Now that property delegates are custom attributes, do you have a vision what can/should happen next with the IB prefixed attributes? Those are part of the language if I'm not mistaken but they should really be part of UIKit/AppKit. One thing that concerns me a little is that @IBOutlet requires the type to be an optional. As a property delegate it could be similar to DelayedMutable.

  • Can you also clarify in the proposal how the API surface marked with property delegates will look for imported API?

    I assume @propertyDelegate attribute itself will be always visible on public types (trivial), but what about properties annotated with a concrete delegate.

    Is this a correct assumption?

    // module A
    @Lazy
    public var foo: Int = 1738
    
    // for the API user the above is exposed as following
    // when module A is imported
    public var foo: Int = 1738
    
  • One other thought: I hope the tooling can teached to jump to the property delegate type declaration when for example in Xcode you would cmd + click on the property delegate.


Small correction, BehaviorRelay is part of RxCocoa which is build as a common extension of RxSwift. That makes BehaviorRelay a safe wrapper around BehaviorSubject that has similar semantics except that it also can be disposed or error out. You can change RxSwift's BehaviorRelay replays to RxCocoa's BehaviorRelay replays. ;)


Other than that I love the direction the proposal is going now, it shaped into something really swifty.

3 Likes

Thanks a lot for building this! One minor suggestion: the character $ looks a bit voodoo. I’d prefer something similar to already existing syntax. For example, the #selector() translates into underlying char * const for the method name. Maybe this makes sense here as well?

Before:

if thingHappened {
  $counter.increment()
}

After:

if thingHappened {
  #propertyStorage(counter).increment()
}
3 Likes

'$' is already used for autogenerated variables in closure.

8 Likes

@pitiphong.p your observation occurred to me when reading the second pitch, too.

@Douglas_Gregor thank you, this pitch is very exciting. I’ve come around to the new attribute way of doing things and happy to access the underlying storage with the $ notation in light of @Jean-Daniel’s observation.

One question I have is this. The swift book suggests how we should read declarations of constants and variables. How should we read attributed declarations?

Can’t wait to see it enter review.

This has to do with custom vs built-in attributes if I remember correctly. The builtin attributes that we have now such as @available and @inline are standard camel case, while custom attributes will be upper camel case. @propertyDelegate will be a builtin attribute. Of course, @IBOutlet breaks that rule, so maybe I am wrong.

A few questions about the storageValue addition.

Given this code:

@propertyDelegate
struct SimpleWrapper<Value> {
    var value: Value
    init(initialValue: Value) {
        self.value = initialValue
    }
}

@propertyDelegate
struct ComplexWrapper<Value> {
    var storageValue: [Value]
    var value: Value {
        get { return storageValue.first! }
        set { storageValue[0] = newValue }
    }

    init(initialValue: Value) {
        self.storageValue = [initialValue]
    }
}

@SimpleWrapper  var simple = 10
@ComplexWrapper var complex = 20
  1. Is this above code well-formed? Specifically, is it acceptable for ComplexWrapper.storageValue to be a stored property and not a computed property? All the examples in the proposal showed it as a computed property.

  2. Are the following inferred type annotations correct?

    let x = $simple  // Inferred type of x is 'Simple<Int>'
    let y = $complex // Inferred type of y is 'Array<Int>'
    

    Assuming that's correct, the presence of storageValue seems to preclude exposing any API on the propertyDelegate type itself, because there would be no way to call it. Instead, you would put such API on the type returned by storageValue. Is that right?

1 Like

Sticking with one character to access the underlying delegate type would keep the Observable use-case cleaner. We have a few options for this, such as #, or more if we're willing to reclaim an operator token such as ^.

An interesting option (which would be source-compatible) would be reusing the & operator.

Existing use-case: Pass the underlying var storage reference
doStuff(&value)

Extension to property delegates: Assign the underlying Lazy instance
var value = &lazy

This would be source compatible, as the behaviour of & would only be different for variables with property delegates. Usage would compose cleanly.
The following code unwraps to Lazy, then to a reference to the var's storage:
doStuff(&&lazy)

Love it. I still don’t understand why exposing the backing $ is important? I think it’s supercool but I just don’t see why I would personality need it.

In the example with user defaults I did not see a need to reset. In the example with CopyOnWrite, I would expect the copy on write to happen with out having to use the store backing. It is possible to model CopyOnWrite property delegate with out going tru the $ store backing?

The only time I was expecting to maybe neededing store backing was if I was composing multiple property delegate but even then this should be limited to be used inside property delegate blessedtypes but I could not find any good examples.

1 Like

In the BehaviorRelay example, wouldn’t this be better model by requiring the store backing to only accept types that conform to the subscriberable protocol? I haven’t tried the toolchain but is this possible?

This revised draft is much improved. Thanks for taking all of the feedback into account!

By default, the synthesized storage property will have internal access or the access of the original property, whichever is more restrictive.

It seems like this rule also needs to take into account the visibility of the property delegate type. For example, what if the property delegate type is fileprivate but the original property is public and the value type is Int? The rule above says the storage should be internal but the property delegate type isn’t visible internally.

Synthesis for Encodable , Decodable , Hashable , and Equatable follows the same rules, using the underlying value of the property delegate type contains an init(initialValue:) and the synthesized storage property's type otherwise.

This rule kind of makes sense for consistency with memberwise initializers and because Decodable will require initialization with the encoded value (and of course Encodable must use the same rule). I do wonder though if this makes sense for Equatable and Hashable. Is there any reason other than consistency with the others to adopt this rule? If not, how important is this consistency relative to other considerations?

Currently, identifiers starting with a $ are not permitted in Swift programs. Today, such identifiers are only used in LLDB, where they can be used to name persistent values within a debugging session.

This is not strictly true. We already have the $0 shorthand in closures. The existence of that shorthand is what made the $ prefix make sense as a compiler-owned identifier space in my mind. It is probably worth mentioning this in the proposal itself.

A property with a delegate can declare accessors explicitly ( get , set , didSet , willSet ). If either get or set is missing, it will be implicitly created by accessing the storage property (e.g., $foo ).

Property observers make sense but I’m less convinced that “overriding” the getter or setter makes much sense. Since property delegates will usually be an implementation detail I think it’s fine to allow it but I am curious what the use cases might be.

A property delegate type delegate access to the storage property ( $foo ) by providing a property named storageValue . As with the value property and (also optional) init(initialValue:) , the storageValue property must have the same access level as its property delegate type. When present, storageValue is used to delegate accesses to the synthesized storage property.

This paragraph is not completely clear to me. Is this an optional feature that allows a property delegate to support direct access to the storage of the value (for optimization, etc)?

Like other declarations, the synthesized storage property will be internal by default, or the access level of the original property if it is less than internal . However, we could provide separate access control for the storage property to make it more or less accessible using a syntax similar to that of private(set)

Given that property delegates will often be an implementation detail I really think this should be supported immediately.

I like that idea. The $ seems a bit "magical" to me, whereas #propertyStorage, while more verbose, is very readable and clear.

5 Likes

The new proposal is well refined and takes the reasonable choices that make the proposal realistic.

In the first pitch I've been excited about property delegates with the Observable delegate in mind, but I no longer believe that it makes sense to define Observable as a property delegate. Observable pattern is not just an implementation detail of the property. For the users of an observable property, the observable's API (like the observe method) is as important as the original property type's API. Observability has to be visible through the type's interface and also through a protocol which is in contrast with the idea of a property delegate.

I believe that the same holds true for the given BehaviorRelay example, which is just a specific kind of an observable.

That being said, I agree with the author's decision on not having a separate access control for the storage property as a part of the proposal.

1 Like

I might have missed that in the original thread, but can you elaborate why this restriction is necessary?

A property with a delegate that is declared within a class must be final and cannot override another property.

Why would this be not valid?

@propertyDelegate
struct Wrapped<T> {
  private let _value: T
  var value: T {
    return _value
  }
  init(initialValue: T) {
    _value = initialValue
  }
}

class A {
  @Wrapped var property: Int = 42
}

class B: A {
  override var property: Int {
    get { return super.property }
    set { 
      print(newValue)
    }
  }
}

I think even this example from the test file is not correct.

class Superclass {
  var x: Int = 0
}

class SubclassWithDelegate: Superclass {
  @Wrapper(value: 17)
  override var x: Int { get { return 0 } set { } } // expected-error{{property 'x' with attached delegate cannot override another property}}
}

You can override the super-class property and you could attach a backing storage. The property delegate will create a new storage and in the overridden getter and setter re-route the overridden property. It does not make much sense, true but this is already possible today.

@Wrapper(value: 17)
override var x: Int { 
  get { return $x.value } 
  set { $x.value = newValue } 
} 

I could imagine someone on this planet has written something like this once even just for fun.

class Superclass {
  var x: Int = 0
}

class SubclassWithDelegate: Superclass {
  private var _x = Wrapper<Int>(initialValue: 17)
  override var x: Int { 
    get { return _x.value }  
    set { _x.value = newValue } 
  }
}
1 Like

One other thing, where does the idea for storageValue originate from? I think this behavior it too magical as it basically allows the compiler to pretend that the storage type is of type of the storageValue property. This is a completely new behavior to the language and I think it makes it less predictable. I think the user should manually call storageValue property instead or mark the entire property delegate type with @dynamicMemberLookup and use key-path member lookup.

1 Like

Thanks for taking the time to assemble this proposal !

I don't know enough about the first two proposals but there is one thing I'd like to note - my $0.02 cents are only about an aesthetical aspect that really bothers me.

  1. Making swift a "Decorator language" is unfortunate in my opinion. I think it's easy to compare to Java where @Decorator syntax is used and abused (and uglifies the code)

  2. the @Lazy / @Whatever syntax is even weirder. Since all other decorates (e.g. @discardableResult) use camel case, and this uses class-case. Even more "Java/Scala" feel here (for better and worse).

  3. The $x syntax also feels highly unnatural to me. I'm sure people could get used to anything but it reminds me so much of other, dynamic, languages.

All code samples I've seen of this being used make Swift feels like a mutated hybrid sort of language, and that "feeling swifty" feel seems to vanish with these additional meta-features.

The more I read into the proposal, I tried to understand "what does it actually solve". Basically, it seems to provide "templating" in a way, for common cases in a code base, but also hides a ton of implementation details.

For example @DevAndArtist mentioned BehaviorRelay as an example, making this code:

var _myValue = BehaviorRelay<Int>(value: 17)
var myValue: Int {
  return _myValue.value
}

be expressed as:

@BehaviorRelay var myValue: Int = 17

But does it really provide a really huge value? Not to my personal opinion, and also it hides important Relay-specific details behind this decorator.

Again, I might not understand everything in this proposal, so I'd love to get a (non-aggressive, hopefully) explanation of anything I've misunderstood.

Thanks!

7 Likes

Some possible mistakes in the proposal:


The synthesized getter will be mutating if the property delegate type's value property is mutating and the property is part of a struct.

It can also be an enum.

@propertyDelegate
enum Foo {
  case a
  case b

  init(initialValue: Foo) {
    self = initialValue
  }

  var value: Foo {
    mutating get {
      self = Bool.random() ? .a : .b
      return self
    }
  }
}

var foo = Foo.a
print(foo.value, foo)
print(foo.value, foo)

@Foo var selfDelegatingProperty: Foo = .a

init(manager: StorageManager, _ initialValue: Value)

Remove _ as initialValue is used as label later on:

init(manager: StorageManager, initialValue: Value)

The proposal should mention a 3rd initialization option, as I think the second option won't be as common as this:

// Simple example
@propertyDelegate 
struct Bar {
  var value: Int
}

class Baz {
  @Bar var property: Int
  init() {
    $property = Bar(value: 42) // explicit, not synthetized
  }
}

If I find anything else I'll extend the list here.

Unfortunately delegate is essentially a reserved word to most Swift developers, so I'd be cautious about using it here. @PropertyAttribute or @PropertyBehavior?

Having lazy and @Lazy coexist in the language seems kind of a nonstarter. I'd expect any solution to gracefully handle this.

Also not loving the $, ha.

3 Likes