Pitch: Property Delegates

The thought I had with $Lazy is that modifiers preceding it would apply to the delegate and modifiers between it and the property itself would apply to the property. One of my examples showed that but maybe didn't explain it enough:

// $foo is private
// foo is public
private $Lazy(closure: {42 })
public var foo: Int

If we do go with the by syntax (or any other trailing syntax) one question I have is how it will handle line wraps such as this:

public var foo: Int
    by private Lazy(closure: {42 })

public var bar: Int
    by private Lazy = 42

Would that be valid (I hope so) or would it be invalid? If we do use a trailing syntax I think I would at least occasionally (and perhaps often) want to place it on a trailing line like this. That said, this example highlights one aspect of the trailing syntax that I don't like: the = 42 is more distant from the actual property's name and type.

One (not great) way to solve that would be to allow the by clause to be placed after the assignment. That looks ok when using line wrapping:

public var bar: Int = 42 
   by private Lazy

but is a little strange when placing the declaration on a single line:

public var bar: Int = 42 by private Lazy

It is also unprecedented in Swift and generally seems like a bad idea.

This means that in trailing syntax the initial value is inherently going to have to lose syntactic proximity to the property. It seems unfortunate to me if we can't find a syntactic solution that doesn't suffer from this liability.

Agree, but maybe (probably?) it's less of a shame than not having composition.

Yes, this is the unfortunate bit. At least the vast majority of use cases could get by with just either PropertyDelegate or MutablePropertyDelegate.

What do you have in mind here?

I would say that non-customizable composition is better than not having composition, wouldnā€™t you? Remember that it will still be possible to manually write specific compositions, including cases where custom logic was implemented.

IMO this approach to composition very much seems to follow the spirit of this proposal even if some use cases are not possible or require manual compositon.

Do you have any thoughts beyond what you said above? Even if this exact approach isn't viable I'm wondering if anything could be salvaged from it to support composition in a different way.

Iā€™m probably over-obsessed with Atomic, but: unless Atomic is implemented with locks, you probably want to implement an atomic differently from how youā€™d implement them separately. But maybe the generic restrictions on Atomic would take care if that.

Yeah, the idea is that a delegate like Atomic that does not wish to participate in composition (because it may have suprising or undesirable behavior if it did) does not have to.

Well, but the reverse composition would be allowed, right?

No, both are opt-in. You need to conform to WrappablePropertyDelegate in order to be used in the inner layer of a composition. You need to constrain your type argument to WrappablePropertyDelegate (and maybe also conform to WrappingPropertyDelegate) in order to be used in the outer layer of a composition.

Each delegate can choose to participate in the inner layer, the outer layer, both or neither. Additionally, constraints can be used to restrict compositions further. For example, a mutable higher-order delegate would need to constrain its wrapped delegate to MutablePropertyDelegate.

In other cases there may be specific semantics that are expected of the wrapped delegate in order for the composition to be sensible. Marker protocols can be used to indicate those semantics. They would be conformed to by wrappable delegates that are able to meet the semantic requirements specified by the marker protocol.

1 Like

Oh, I see that now, thanks.

I think we can probably do without this much complexity in the short term.

Thatā€™s fair, but it would be good to support composition someday (whether it looks anything like this or not).

1 Like

Honestly, I donā€™t know if thereā€™s any sensible language feature that could capture what the built-in lazy does. We should not have allowed it to capture self in this manner; no other initializer can.

Doug

I understand this sentiment from a compiler and language development perspective, but it is undoubtedly useful (and used, in production, today, often) with the current formulations of Apple APIs w.r.t. two-phase initialization. If using self in the built-in lazy were impossible, and produced an error, people would still reach for it because itā€™s natural. Thatā€™s a problem for this proposal if it seeks to supplant the built-in lazy. Even if doing so were an explicit non-goal, a feature that yielded two lazy constructs would have that discussed constantly in the proposal review, no matter how good the reasons are, and to the detriment of the proposalā€™s success.

6 Likes

I thank Brent here for his always-incredibly-thorough analysis! There are options here.

I really donā€™t want us to get so wrapped up in finding the perfect obscure twist of English to use for this feature.

I absolutely love the API Naming Guidelines and use them whenever they are relevant, but Swift code is still fundamentally code, and we should lean towards syntax that is easy to communicate first and reads as good prose second. We have class Foo and not class named Foo for this same reason.

I, myself, would throw behind by. I think itā€™s perfectly adequate to communicate what it does to a variety of English fluencies, or worst-case is exactly as obscure as the in sigil thatā€™s already in the Swift. I can imagine other uses of by in the future. Thatā€™s harder to imagine with the more purpose-built sigils.

1 Like

@Douglas_Gregor is it a bad idea to loosen the restriction about the $ identifiers a little bit more? I would really like it if we could write delegate storages manually as well as the current proposal want to introduce quite a lot from the beginning with quite a few constraints, which solely originate from the automatic synthetization. On the other hand if one would write a $ prefixed identifier for a property delgate, the compiler would requires only two things:

  • you have to declare a pair of identifiers (stored one prefixed with $ and computed one without any prefix but with the same name)
  • the type of the stored property must have the @propertyDelegate attribute (that will require the value property on the delegate and it will also tell the compiler if its type matches with the non-prefixed computed property from the previously mentioned pair)

As mentioned in my other post, the compiler would only need to generate a single synthetization for the get/set of the non-prefixed computed property. That will not require any further constraints on the core feature and of cource prevent the need of access modifiers when you want to expose the baking storage to the higher access level. It would also make the mental model quite simple.

If you then wanted to expose the backing storage you could add the by Delegate after the type which will tell the compiler to check wether the $ prefixed identifier has the same access level as the non-prefixed identifier and emit an error if they mismatch.

Moving forward we can work out the synthetization part, which is currently proposed but from my point of view is highly controversial as itā€˜s the core mechanism that generates a lot of constraints already. With this would then eliminate the need for you to write $ prefixed declaration manually.

I think weā€˜re tackling the proposal in a reverse order right now as we constrain us too much by a magical behavior of the synthetization. If you recall synthetization for Equatable/Hashable was introduced after we had years of experience with writing custom hash values and == functions which we could analyse before working out what and how we want that to be synthesized. In this proposal weā€˜re starting it right away which can, but donā€˜t necessarely have to, go really wrong.

If you could not follow my idea path, I could write a small document with more examples. I really think we should go down that road and learn and observe on how we really want the synthethization to apply and what chalenges we would need to solve to cover 'most' of the community generated property delegates.


About the bikeshedding of the keywords here: why do we need a "keyword" here at all?
Were other non-keyword options considered?

  • Value ~ Delegate
  • Value ~> Delegate
  • Value :: Delegate
  • Value @ Delegate

Not sure if this keywoed was already mentioned above:

  • Value at Delegate

Maybe that's a monumentally stupid idea but I still wanna throw it in here. Couldn't the delegate be specified like property observers and replace get/set there?

public var kermit: Muppet {
   public @usableFromInline delegate { return Lazy(closure: { 42 }) }
   didSet { ā€¦ }
}
1 Like

I have always liked a lot of the current ordering, i.e ā€public lazy var foo...ā€, particularly that the lazy ā€behaviour modifierā€ is before var.

And would prefer to keep it that way. So I support Johnā€™s approach or something similar to it.

Questions and thoughts:

  1. Can a delegated property have willSet/didSet observers? That is:

     var foo: Int by MagicBehavior {
       willSet { print("becoming \(newValue)")) }
       didSet { print("was \(oldValue)") }
     }
    
  2. Can a delegated property be designated @objc dynamic to support Key-Value Coding and Key-Value Observing? We need KVC to delegate @IBOutlet and @IBInspectable properties. We need KVO to participate in Cocoa Bindings.

  3. @IBOutlet properties are typically IUOs.Can a delegated property be an implicitly-unwrapped optional? That is:

     @IBOutlet var foo: UIView! by MagicBehavior
    
  4. Can we support trailing closure syntax on the delegate's initializer? That is:

     struct Lazy<Value> {
         init(_ body: @escaping () -> Value) { ... }
     }
     
     var foo: Int by Lazy { 2 * 2 }
    

    Seems unlikely if we support willSet/didSet observers.

  5. Why not just an underscore prefix for the delegate storage property instead of the magic $ prefix?

    Upside: It's already common for a computed property foo to use _foo as its storage. The pattern will already be familiar to many Swift users.

    Downside: In existing code that has both foo and _foo properties, if I want to change foo to be delegated but still need the existing _foo, I have to rename the existing _foo.

  6. There's a pragmatic benefit to using a PropertyDelegate protocol declared in the standard library instead of an attribute: documentation. If there's a magic PropertyDelegate protocol, you can document the feature in the standard library's PropertyDelegate.swift. Then a user who's not familiar with PropertyDelegate but knows how to either show quick help or jump to definition in Xcode can learn about it without having to go to a browser to google it.

  7. It doesn't seem far to go to get lazy-like access to self. I'll use ā€œcontainerā€ to mean the object containing the lazy property. Then:
    a. Add a second type parameter, for the container's type, to the delegate type.
    b. Instead of a value property in the delegate, use a getter method and a setter method. The getter is passed the container. The setter is passed the container and the new value.

    Note that both properties (foo and $foo) are still subject to the two-phase initialization rules. The initializer of the delegate ($foo) doesn't receive the container as a parameter, and the container can't use the delegated property (foo) at all until after phase one of initialization has ended.

    Then we can implement Lazy with container access:

     enum Lazy<Value, Container> {
         case uninitialized((Container) -> Value)
         case initialized(Value)
     
         init(_ initializer: @escaping (Container) -> Value) {
             self = .uninitialized(initializer)
         }
     
         mutating func value(in container: Container) -> Value {
             switch self {
             case .uninitialized(let initializer):
                 let value = initializer(container)
                 self = .initialized(value)
                 return value
             case .initialized(let value):
                 return value
             }
         }
     
         mutating func setValue(_ newValue: Value, in _: Container) {
             self = .initialized(newValue)
         }
     }
    

    And use it like this:

     class Person {
         let firstName: String
         let lastName: String
         init(firstName: String, lastName: String) { ... }
     
         var fullName: String by Lazy({ $0.firstName + " " + $0.lastName })
     }
    

    We can constrain Container as needed. For example:

     import AppKit
    
     struct DisplayProperty<Value, View: NSView> {
         init(initialValue: Value) {
             self._value = initialValue
         }
    
         func value(in view: View) -> Value { return _value }
    
         mutating func setValue(_ newValue: Value, in view: View) {
             _value = newValue
             view.needsDisplay = true
         }
    
         var _value: Value
     }
    
3 Likes

Replying to some of your points.

(1) no as the computed property will have synthetized get/set, which do not allow willSet/didSet observers (you could move these into the delegate itself though).

(3) see my example above and what @Douglas_Gregor has replied to it. An IBOutlet delegate can potentially eliminate/hide the use of optionals in general.

(5) what if you want to have a delegate for _foo, will its delegate be __foo?

The willSet/didSet observers can use self. The proposed delegates can't.

I was going to write about replacing @IBOutlet (and @IBOutletCollection and @IBInspectable) with property delegates, but the proposal doesn't allow composition of delegates. If @IBOutlet were superseded by a @propertyDelegate struct IBOutlet, I wouldn't be able to delegate my outlet to any other delegate type. If delegate composition becomes allowed, or if outlet-ness (and inspectable-ness) becomes something my own delegate types can provide, then I agree that an IBOutlet delegate type is the way to go.

Yes, why not? Is __foo worse than $_foo?

I pointed out my concerns about some of these limitations and how we can potentially solve them, but Iā€˜m not sure what the community thinks of such approach.

I thought about the issue with IBOutlet delegate as well then I realized that it likely be IBOutlet<Value> and in one of the examples from this thread you can see that you can 'nest' delegates. That way you can create an own delegate that warps the general 'IBOutlet' delegate to extend its functionality.

@Douglas_Gregor said here and here that delegates can't be composed.

You donā€˜t need composition when you can re-use by deep nesting delegates.