Pitch: Property Delegates

I'm still unclear on what the use case is for internal/public storage properties. Does someone have an example of when that might be useful?

4 Likes

Regarding the discussion of $foo and what it's access level should be, why give access to the backing store at all unless the programmer creates it? Then the whole thing simplifies and the argument becomes moot...

Instead of

private var $foo:Delegate //Compiler automatically creates this as storage whether I need it or not
public var foo by private Delegate = 10

just:

//compiler creates a storage variable without a programmer accessible name
public var foo by Delegate = 10

If you need access to the backing store for some reason then you create it yourself:

private var myStorage = Delegate(10) //This is just a normal var and I can give it whatever access level I want
public var foo by myStorage

To me, this feels a lot more predictable and less magical... and we aren't burning $

6 Likes

I rather prefer this being aligned with macro syntax; I believe if/when user-defined macros become a swift feature, this property delegates feature becomes a legacy version of that more powerful feature.

In that alternate future, @propertyDelegate (however it is spelled) would be an attribute telling the compiler to convert that type into a property-viable macro.

Writing Observable properties without the need to write a addObserver/removeObserver wrapper for each one (as the storage will already expose these methods).

3 Likes

Another example would be @Atomic, assuming we can find a way to make atomic types sound as a general matter. It's certainly useful to be able to simply load and store the underlying value, but the Atomic delegate type would also provide operations like exchange and compareAndExchange, as well as loads and stores with explicit memory-ordering semantics. While it's generally good practice to encapsulate atomic properties, that includes the abstract storage and not just the backing storage.

6 Likes

Would it be problematic to allow an optional access control keyword at the beginning of the parenthesized arguments?

@Lazy(private) 
public var x = 10

@UserDefault(fileprivate, key: "FOO_FEATURE_ENABLED", defaultValue: false)
static var isFooFeatureEnabled: Bool

If omitted the delegate would default to the minimum of internal or the property's declaration. That looks broadly consistent with our sort of weird hand-wavy attribute syntax practices β€” e.g. @available(swift, introduced: 5).

1 Like

This breaks the symmetry with normal initializer calls. If we’re going to support explicit access control I think private(storage) is the way to do it. Are there good arguments against that syntax?

3 Likes

Ah, akin to private(set). So those would be:

@Lazy
public private(storage) var x = 10

@UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
fileprivate(storage) static var isFooFeatureEnabled: Bool

That looks good to me, as well.

How would something like a UserDefault property delegate handle dynamically-named keys? All the examples thus far have been using static keys. But, for example, in a document-based app you'd want to name-space keys with a document identifier, which cannot be known at compile time and will change for every document.

If the document identifier is known at initialization time, and assuming this should be an instance property instead of a type property (which seems right for a per-document thing), you could do something like this:

@UserDefault
fileprivate(storage) var isFooFeatureEnabled: Bool

init(documentID: UUID, ...) {
  $isFooFeatureEnabled = UserDefault(key: "\(documentID).FOO_FEATURE_ENABLED", defaultValue: false)
  ...
}

Future improvements might make this more elegant, e.g.:

var documentID: UUID

@UserDefault(key: { "\($0.documentID).FOO_FEATURE_ENABLED" }, defaultValue: false)
fileprivate(storage) var isFooFeatureEnabled: Bool

3 Likes

Has it come to the attention of anyone that these implementations of Lazy and UserDefaults waste memory? Flatten them a bit and you get this for Lazy:

class C {
    var somethingToCheck: Bool { ... generated accessors ... }
    var $somethingToCheck = Lazy<Bool>(initialValue: { ... some closure ... })
}

Notice how Lazy ends up storing its closure within the class instance. The same closure will be stored in each instance of the class. Wouldn't it make more sense for the closure to be stored only once as a static variable? You'd probably end up saving 15 to 16 bytes in the case above.

Same can be said about UserDefaults:

class C {
    var someSetting: Bool { ... generated accessors ... }
    var $someSetting = UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
}

Here again UserDefault ends up storing the same key and same defaultValue within each class instance.

Should we assume the compiler will notice what's going on and manage to optimize all this away? That seems unlikely to me, and I don't find this very appealing.

3 Likes

I wish!

So as I understand it, this is essentially a Python descriptor, which can also be used like a decorator as long as the function doesn't take any arguments.

If property delegates were marked using a protocol, we could decouple the generic parameter from the property's type, which would allow us to return a wrapper function:

protocol PropertyDelegate {
  associatedtype PropertyType
  var value: PropertyType { get }
}
// Not shown: MutablePropertyDelegate

struct Profiled<T> {
  var runtimes = [TimeInterval]()
  let decorated: () -> T

  init(_ fn: () -> T) {
    self.decorated = { 
      let start = NSDate()
      let result = fn()
      runtimes.append(NSDate().timeIntervalSince(start))
      return result
    }
  }
}

extension Profiled: PropertyDelegate {
  typealias PropertyType = ()->T
  var value: ()->T { return decorated }
}

struct Something {
  @Profiled { 
   // Lots of code, long-running function.
   // ...
  }
  var myFunction
}

let thing = Something()
thing.myFunction()
thing.myFunction()
thing.myFunction()
print(thing.$myFunction.runtimes)

I'm not sure if this is the most current proposed syntax, so perhaps I'm repeating things others have said, but this syntax doesn't look amazing.

With reference to the point about feature-creep: of course we cannot consider arbitrary future features, but this is a significant one (especially for server frameworks and scripting) that would likely see far more usage than lazy/NSCopying. Hopefully it is reasonably focussed: the mental model is essentially the same - just as a "property delegate" wraps some underlying property storage, a decorator wraps an underlying function.

Once the proposal is updated I'll be able to say more. Right now it's difficult to find the "latest version".

I've been thinking about this more, and my argument against your generalization was really poor. I think it's important that we allow for the inference in the common case (@Lazy var x: Int rather than @Lazy<Int> var x: Int), but if the user is going to specify the full type of the property delegate type, it's completely reasonable to get the original property's type from the value of that type.

Sorry for not seeing this earlier. My newer implementation handles things this way, and the upcoming revision of the proposal drops the restrictions.

Thank you,
Doug

4 Likes

Ah it was a reply to the reply to my reply :D Discourse wasn't able to notify me.

That relaxation makes the requirement of the single generic type parameter completely optional, so you can have 0 to n generic type parameters. So it's best that the types are always matched against value which is strengthened by the access level rules as value has the same access level as the property delegate type. In most common cases you'll be able to omit the generic type parameter list, which is what you initially wanted, but in other cases the user will be able to create more powerful/flexible property delegates.

For an IBOutlet of type MyView, it would:

  1. Hide the optionality of MyView (which is currently necessary, because these properties are set by the story board as a second step, after object initialization.
  2. Error out when accessed before set.
  3. Error out when set a second time.

See this comment for an example: Pitch: Property Delegates - #34 by DevAndArtist

Sorry for late post....Has this been accepted?

Search for "property wrappers" which is the accepted feature that result from a lot of changes and discussion since this first attempt.