[Pitch #2] Property Delegates by Custom Attributes

The proposal itself won't introduce the type Lazy, it's just used as an example what property delegates will enable. The only advantage lazy has over the property delegate type Lazy is that it allows access to self. If the proposal would be accepted, then I think we can start a separate debate whether we want stdlib to have a Lazy type or not.

Catching up after abandoning the previous two threads for high volume. Mostly I like it, and while I still have concerns about claiming attribute syntax for this purpose it'll probably work okay in practice. (I do think you should explicitly spell out the admonition to avoid lowercase names, and that the compiler should warn on it the same way the C++ compiler warns on user-defined literals without a leading underscore.)

The one thing I'm completely confused about is storageValue. It seems like this shadows the delegate instance itself, which feels weird and harder to learn, and none of the example delegates make use of it except CopyOnWrite, where it's a relatively minor convenience.

And

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

I also feel like this wouldn't strictly be necessary, but if it simplifies things for now then okay.


Nitpicks:

  • Per previous comments by @David_Smith, the UserDefault example needs more work to be a good client of Foundation.UserDefaults, so if you want to use this as an example it probably deserves caveats.

  • UnsafeMutablePointer as a property delegate makes me very unhappy, but maybe it shouldn't.

  • Someday, someday, move-only types, and then UniquelyOwnedPointer can be a property delegate.

5 Likes

We've generally saved # for more macro-y uses, which is why we don't want to use it here.

7 Likes

Have anyone considered how patterns like this are used in the wild, E.g. in the compatibility suite?

I’d expect most use cases where a computed property exposes access through indirection to a custom storage wrapper of some kind, that the storage in question in private or file private?

I can’t think of any examples where I would want to expose the underlying storage, and if the initial implementation of this doesn’t provide a way of declaring access level for the underlying property delegate, I’d prefer the initial default to be private.

Most built-in attributes are lowerCamelCase in Swift. The ones that are UpperCamelCase (like IBOutlet) are those associated with a particular framework, and which we’d like to have implemented in the framework at some point.

The intent is for the lowerCamelCase ones to remain reserved for the compiler, and UpperCameCase will become available for custom attributes.

3 Likes

I’d call them variables or properties. Property is the more general term.

  • Doug

Isn't the compiler-generated lazy competent enough to avoid storing its closure as part of the object? The one in the proposal wastes storage space for properties of types less than double-pointer-size.

Edit: same could be said of UserDefault which will store the same user default string and the same default value in each instance whereas it should require no storage at all.

Yes, the built-in lazy avoids storing the closure by effectively inlining it into the getter. Did the proposal not make this sufficiently clear?

  • Doug
1 Like

This idea that the compiler creates another variable called $foo is still very complex and magical to me. I still feel like we shouldn't directly have access to the backing var unless we create it ourselves. In particular, having to matchup the names seems problematic for refactoring...

However, if we are absolutely intent on burning $, how about the following as a (hopefully less confusing) mental model:

  1. Use $behaviorName to declare a behavior on a property (as opposed to @behaviorName). This will avoid conflicts with future @attribute names
//Compiler makes a backing var, but it isn't directly accessible to the programmer
$MyBehavior var foo:Int = 12

If I need to call some function on the behavior itself, we use $ as a postfix operator:

foo$.reset()

Nested behaviors can be accessed by repeatedly applying the operator:

foo$$.someMethod() //This acts on the nested behavior

Advantages:
• No conflicts with @attributes
• Works with nested behaviors
• I don't have to know about the backing store (and I can still access methods on the behavior)

I was simply pointing out to @DevAndArtist that access to self wasn't the only advantage of the built-in lazy.

More generally, I'd say the Lazy and UserDefault examples highlight a shortcoming of the proposal by forcing things to be part of the storage when they shouldn't. They're what I call bad examples of things you can do with a property delegate. You want a property "delegate" but you need an instance of the delegate for every instance of the property, so anything the delegate needs has to be duplicated everywhere.

I'd be happier if the delegate and the storage were two distinct types: the delegate containing things that need to be stored once, statically, and the storage (provided by the delegate) storing the data of the property (if any) within its container. I suggested something like this earlier, but I'm not sure anyone noticed. It's more complex and has to forgo subscript access (because no inout parameters in subscript), but the two-separate-types aspect is rather convenient. Maybe I should try harder to fit the subscript in that design...

7 Likes

I think this proposal is great! Two thoughts:

  • This has been brought up in this thread and the previous one, but I don’t know that it’s been officially addressed: is there a danger of confusing the naming in this property delegates proposal with that of the existing concept of delegate properties (ie, weak var delegate: FooDelegate?). The latter is found more in Cocoa than in Foundation.swift, but it’s still quite prevalent, and I think it could be confusing to talk about/search for, especially for people new to the language. (It was actually a little difficult to figure out how to differentiate them in this paragraph, haha.)

  • As far as the synthesized storage property name, I much prefer _ over $, for two reasons:

    • As noted by others, $ is currently used as a shorthand argument name in inline closures. That is conceptually different from its use here, where the $ denotes the actual name of the property storage, rather than a syntax convenience. I don’t think there’s any institutional knowledge a user can bring over from inline closures to $fooStorage, and the existing syntax overlap might be a source of confusion itself.

    • _ is already used, by users and in Foundation.swift itself (for example), to denote private, custom property storage. Why not bake it into the language a bit? I think its purpose is a bit more obvious at the callsite than $, due to its familiarity.

3 Likes

That would create possible conflicts between language attributes and user defined propertyDelegate.

What should the compiler do with @PropertyDelegate if a user create a property delegate named PropertyDelegate ?

1 Like

A simple example: caching computed property.

@Memoized would compute the value on first access. Then stats change and the cached value should be invalidated to be recomputed if needed on next access.

This is a tumbled version of my two-type suggestion from earlier. There's one type for the delegate, and it contains an inner type called Storage. The delegate is stored statically while the storage is stored inline where the property is.

Unlike the previous iteration I came with in the pitch #1 thread, this one does not give access to self, but allows a subscript to be used to access the property's value instead of custom get and set functions.

@propertyDelegate 
struct Lazy<Value> {
  enum Storage {
    case uninitialized
    case initialized(Value)

    subscript (delegate: Lazy) {
      get {
        switch self {
        case .uninitialized:
          value = delegate.initialValue()
          self = .initialized(value)
          return value
        case .initialized(let value):
          return value
        }
      }
      set {
        self = .initialized(newValue)
      }
    }
  }

  var initialStorage: Storage { return .uninitialized }
  var initialValue: () -> Value

  init(initialValue: @autoclosure @escaping () -> Value) {
    self.initialValue = initialValue
  }
}

Then this code:

$Lazy var p: Int = 5

Would generate something like this:

// the delegate lives as a static variable
static var _p_delegate: Lazy<Int> = Lazy(initialValue: 5)
// the storage is an instance variable
var _p_storage: Lazy<Int>.Storage = Self._p_delegate.initialStorage
// the computed p accesses the value through the delegate
var p: Lazy<Int>.Value {
   mutating get {
      // mutating only when Lazy.get takes `self` as `inout`
      return _p_storage[_p_delegate]
   }
   set {
      // mutating only when Lazy.set takes `self` as `inout`
      _p_storage[_p_delegate] = newValue
   }
}

An approach like this could line up well in a world where custom attributes are statically stored structs: the "property delegate" attribute would be a custom attribute like others with the addition that it provides a storage type.

I really like what I see here, thank you to everyone who has put hard work into this.

Has there been any thinking about how multiple property delegates could be composed? Would it require using a hypothetical @propertyDelegate struct Compose<P, Q>, or is this a feature of the syntax I missed?

I sketched out one approach to composition using a family of protocols to support higher-order property delegates. Now that the restriction on a single generic argument of Value has been removed I think something along these lines would work. I think this is an area that will need to see some exploration and experience before anything along these lines would be considered as a proposal though.

1 Like

+1

I liked the first one. I love the second one!

1 Like

I have to admit that, at first sight, I'm not thrilled by the appearance of (say) @Lazy and @propertyDelegate near each other:

@Lazy var foo = 1738
@propertyDelegate
enum Lazy<Value> {…

because @Lazy uses a user-chosen name, and @propertyDelegage uses a keyword-ish name. There's something … "untidy" … going on there.

OTOH, @IBOutlet in the general vicinity of @objc doesn't really bother me, so maybe I'll get used to the new syntax.

3 Likes

On a separate note, I'm another person who finds $foo kind of ugly and random (even though I understand it's not really random).

What about using a name with a different structure? Specifically, call the property delegate foo.$0 instead of $foo.

The $ symbol is still there, for whatever semantic weight the proposal already prizes, but it's something rather similar to other uses of $ in closures.

Note that I'm not necessarily suggesting that foo.$0 would be any kind of property of foo at the implementation level. I'm just suggesting a magic syntax to name something that needs a magical name.

Interesting proposal!

I have a common pattern with properties for observable (reactive) variables, which I don't think are covered by this. However, after reading the comment by @Srdan_Rasic about Observable, maybe property delegates aren't supposed to handle this?

Nonetheless, it might be interesting to see if this is related.


I have a Variable (similar to BehaviourRelay) that can be read/observed, and a separate VariableSource which is used write to the variable.
By having these to different types, I can control who can read and write, and pass around read-only reactive variables, for multiple clients to observe.

// Read-only observable variable, that can be freely passed around
class Variable<T> {
  var value: T { get }
  func subscribe(_ handler: (T) -> void)
}

// Writable "source" for the observable Variable, keep this private
class VariableSource<T> {
  var variable: Variable<T>
  var value: T { get set }
}

And this is how it's used:

public class Foo {
  // Keep source private, only Foo can modify bar
  private barSource = VariableSource(value: "initial")

  // Anyone can read/observe bar, or pass it around
  public bar: Variable<String> { barSource.variable }

  public func changeBar(x: Int) {
    barSource.value =  "Hello \(x)"
  }
}

let foo = Foo()
print(foo.bar.value) // Print current value
foo.bar.subscribe { x in print(x) } // Print future values

other.handleThis(foo.bar) // Other code can also observe

As far I as I can see, this proposal doesn't cover this scenario, with all the proper access controls.
I could write something like this:

public class Foo {
  @Variable public bar: String = "initial"

  public func changeBar(x: Int) {
    bar =  "Hello \(x)"
  }
}

let foo = Foo()
print(foo.bar) // Print current value
foo.$bar.subscribe { x in print(x) } // Print future values

other.observeThisThing(foo.$bar) // Other code can also observe

foo.$bar.value = "bad" // Don't want to expose this!

But this also exposes the setter, any code that has access to $bar can now set its value, not just Foo.

Again, I'm not sure if this proposal is supposed to cover this, or even that it should.
But this is a common pattern I use a lot, with Variables, Promises/Future and CancellationTokens. So I wanted to leave this use case as an example.

2 Likes