[Pitch #2] Property Delegates by Custom Attributes

Proposals already come with implementation and often provide pre-built toolchains. While not a full "Boost" process, it's something people can try out and experiment with. To me it's important to scope down a pitch/proposal to something that can go in to swift proper. But then additional features building on top of it could live even a bit longer (with pre-built toolchains) as a place for experimentation and get experience before integrating them to swift.

Yeah, I get it - everyone wants everything done now now now! :-) . I don't see a problem with rolling this out in stages and learning along the way though, if nothing else, it makes the discussion threads easier to digest because there are fewer moving pieces in the debates.

In any case, I +100 love this feature, it will be a huge boon to expressivity of the Swift language. Thank you for driving it!

-Chris

5 Likes

I think this is a pretty significant flaw with storage value. What if we allowed storageValue to be optionally mutable and also allowed delegates to opt-in to init(storageValue:). This initializer would support out of line initialization via the storage value. It wouldn’t cover every case (including your Box example) but it would be better than not supporting out of line initialization when the storageValue is different than the delegate itself.

I’m also curious why you chose to expose both Box and Ref in the example. It’s possible to do something similar that only exposes a single Ref property delegate that uses itself as the type of storageValue:

private class Box<Value> {
    var value: Value
    init(_ value: Value) { self.value = value }
}

@propertyDelegate
struct Ref<Value> {
    private let base: Base
    private init(base: Base) { self.base = base }
    
    init(initialValue: Value) {
        base = Derived<Value>(box: Box(value), keyPath: \.self)
    }
    
    init(storageValue: Ref<Value>) {
        base = storageValue.base
    }
    
    var value: Value {
        get { return base.value }
        nonmutating set { base.value = newValue }
    }
    
    var storageValue: Ref {
        get { return self }
        set { self = newValue }
    }
    
    subscript<U>(dynamicMember keyPath: WritableKeyPath<Value, U>) -> Ref<U> {
        return base.makeRef(appending: keyPath)
    }
    
    private class Base {
        /* abstract */ var value: Value {
            get { fatalError() }
            set { fatalError() }
        }
        /* abstract */ func makeRef<NewValue>(appending keyPath: WritableKeyPath<Value, NewValue>) -> Ref<NewValue> {
            fatalError()
        }
    }
    private class Derived<Root>: Base {
        let box: Box<Root>
        let keyPath: WritableKeyPath<Root, Value>
        init(box: Box<Root>, keyPath: WritableKeyPath<Root, Value>) {
            self.box = box
            self.keyPath = keyPath
        } 
        override var value: Value {
            get { return box.value[keyPath: keyPath] }
            set{ box.value[keyPath: keyPath] = newValue }
        }
        override func makeRef<NewValue>(appending keyPath: WritableKeyPath<Value, NewValue>) -> Ref<NewValue> {
            return Ref<NewValue>(
                base: Ref<NewValue>.Derived<Root>(
                    box: box,
                    keyPath: self.keyPath.appending(path: keyPath)
                )
            )
        }
    }
}

@Douglas_Gregor if we‘re really going for shadowing approach using an extra optional property on the delegate, can we rename storageValue to delegateValue? I think the latter is the closest name for its behavior. Also the Box example defeats the behavior of storageValue and leans toward delegateValue.

@propertyDelegate - delegateValue
@propertyStorage  - storageValue
@propertyBehavior - behaviorValue
// you get the idea

I like the feature overall, but I am still very uncomfortable with several aspects (and would vote against it in it's current form):

  • I find $foo to be magical and confusing. I would much rather see a post-fix operator that allows you to access the storage of foo: foo$. The characters are the same, but the mental model is much simpler. I am dealing with an operator like ? instead of having to know about compiler generated variables that are invisible.

  • I really think this should be defined using a protocol (or family of protocols) instead of an attribute. That should allow composition of delegates.

  • The delegate really should have a pair of functions for get/set instead of a property. In addition, those functions should be passed the self of the enclosing type. I know dealing with that self is a possible future direction, but it should be the other way around: Self should be available from the start, and a later version can provide a more efficient route for delegates that don't need self. Why limit the feature unnecessarily with such a major limitation for a very minor speedup?

  • I'm uncomfortable with the potential overlap of @Attributes and how to deal with that, especially considering the custom attribute proposal happening now. Attributes also afford stacking in a way that isn't allowed with delegates (but will be needed/allowed with other attributes). I feel pretty strongly like we will regret this choice down the road...

  • Instead of the possibility of having two access modifiers on a declaration (what a nightmare that would be), we should just have an attribute on the functions of the property delegate itself that declares when something is actually meant to be accessed using $. (e.g. observe or reset). Otherwise it shouldn't be available to call unless you create the type yourself.

4 Likes

Actually, now that I think about it, a much simpler model overall would be:

  1. The programmer doesn't have access to the storage at all unless they create it themselves and assign it to the property. (i.e. there is no $foo only a nameless compiler variable)

  2. Within a type which is a property delegate, you would be allowed to create functions who's name starts with a $, and those functions could be called on the property when needed.

struct MyDelegate<T,S> : PropertyDelegate {
    typealias Value = T  //Having this as an associated value lets us limit the values it can be used with. For example, we may have a delegate that only works with strings
    typealias Enclosing = S  //Having this as an associated value lets us limit the types our delegates can be placed on

    func delegateValue(enclosingSelf:S)->T {...}
    mutating func setDelegateValue(_ newValue:T,  enclosingSelf:S) {...}

    func myFunc() {...} //Only callable to those that create the type
    func $reset() {...} //This can be called from the property
}

myType.foo.$reset()  //We can call the $func on the delegate using it's name (which we know won't conflict)

The advantage of this design is that it lets the designer of the delegate choose what needs to be exposed as part of controlling the delegate. If you needed/wanted to call one of the non-$functions, you would create the delegate yourself and then assign it to the property.

6 Likes

This is an interesting approach to allowing delegates to expose API without introducing potential for conflicts with the property’s own API. I like how it ties the delegate API more directly to the delegating property. I’m not sure how it would work for use cases like Ref which use key path member lookup. We can’t prefix a subscript with $ and if we could it wouldn’t be compatible with key path member lookup. If we consider this approach I think it needs more refinement.

Do you have any idea how this approach would support direct (potentially out of line) initialization of the delegate? It also isn’t clear how a user of the delegate could control visibility of the delegate API on the property using the delegate. I suppose we could say private(storage) makes the $-prefixed delegate APIs private (and so on for other access modifiers).

1 Like

It's that; imho it's better to either have "all magic" (a underlying property with a name that's forbidden for normal users), or no "magic" rules (like you have to declare at least the name of the actual field, and nothing is done automatically).
$ has a precedent in closures, whereas _ is just an arbitrary character.

It's just the ugly look ;-)

3 Likes

With the old by syntax it would be simply replacing the delegate type with the backing variable:

private var myDel = MyDelegate(startingValue: 11)
var foo:Int by myDel //Instead of MyDelegate = 11

With this new syntax, I suppose the best thing would be to have a form which takes a backing variable:

private var myDel = MyDelegate(startingValue: 11)

@MyDelegate(using: myDel)
var foo:Int

In terms of the visibility of $-prefixed functions, they would always be visible where the property is visible. However, you could have non-prefixed functions that are accessible privately used only within the property's type, and you would use them by creating your own backing variable and calling methods on it.

But yes, $ would mean that it is designed to be called externally.

I have to admit, I don't fully understand the use case for Ref, so the following may not be satisfactory...

Subscripts and keypath lookup would still be available to the type by creating the backing variable yourself and then calling them on the backing variable. You could then expose it, either by exposing the backing variable, or through a property which calls it indirectly.

public private(set) var fooRef = Ref<Rectangle>(startingValue: ...)

@Ref(using: fooRef)
public var foo:Rectangle

print(foo) //Accesses the rect
print(foo.topLeft) //Accesses the rect's top-left point

let rectRef = fooRef //get the Ref<Rectangle> 
let topLeftRef = fooRef.topLeft //get the Ref<Point> for the rectangle's top left

Basically $-prefixed functions are the 80% common use-cases that you want to expose to all users of the property (e.g. foo.$observe {...} and foo.$reset()). For the 20% advanced use cases that require the type to have access to levers that external users of the property don't, then you create the variable yourself, and call it as needed.

If you need to hide levers which are normally public, then you would make the property itself private and forward to it (possibly using a property delegate):

@MyDelegate
private var secretFoo:Int //We can only call secretFoo.$reset() where we can see secretFoo

@Forward(to: \.secretFoo)
public var foo:Int //This is public, without access to MyDelegate's $reset()

This shouldn't be necessary very often at all though if the delegate is designed properly. In the above case, if this were needed often, someone would quickly make a delegate with reset() instead of $reset().

If we decide that there are too many use cases which need private access to the backing variable that having to create it is prohibitive, then we could also add the post-fix $ operator to allow access (only within the property's type) to the compiler's backing variable.

@MyDelegate
var foo:Int

foo$.reset() //This can only be called within the type
foo.$observe {...} //This can be called anywhere foo can be seen

This has the obvious issue that $. and .$ are very similar, but in practice there will be relatively few $-prefixed functions, and $. can only be used inside the type... so the compiler will be able to catch any mistakes easily. (the only exception being if you have both $reset() and reset() that behave differently)

Really like the proposal based on a quick read (also read the original property behaviors proposal)

One small thing though concerning out-of-line initialization, wouldn't that be a bit weird in the example below? The same line means two potentially quite different things

@Foo var x: Int
var i = 0
repeat {
    x = i // Either `init(initialValue: …)` or `value = …` depending on iteration
    i += 1
} while i < 3
print(x)

I had meant for storageValue to permit mutability (the implementation supports it, too), but adding init(storageValue:) with a definite initialization rule to handle the initialization side is very cool.

They felt like separate concepts to me: Ref being an abstracted reference, Box being one way in which we could allocate storage that could be referenced by Ref.

Doug

It ends up calling init(initialValue:) all three times, destroying the prior object for the last two iterations.

Doug

@Douglas_Gregor any feedback regarding a different name for storageValue? I really think this property should be called delegateValue as it resembles more its semantics.

I agree that delegateValue is a better name.

Doug

1 Like

@Douglas_Gregor can you clarify how delegateValue actually works? Why does the compiler know that $property should be seen as delegateValue? If I use an instance of such delegate not as a property delegate but as a plain type, the compiler won't opt-in into this new shadowing behavior right?

It's essentially the same thing as with value: the real storage property gets a truly-hidden name (heh, we could call it $$property) and $property wraps access to it:

var $property: Ref<Int> {
  get { return $$property.delegateValue }
  set { $$property.delegateValue = newValue }
}

That's correct.

Doug

If you’re really serious about $$property maybe it should be hidden except for out-of-line initialization. That seems a little strange, but being unable to initialize the delegate out-of-line isn’t great either...

One more thing, this idea has not to ship in the first implementation or ever, but I'd like to explore it at least here publicly. Can we potentially improve the way how we can have a 'safer' property delegate that will have a non-optional value, but the delegating computed property can stay optional?

For example UIApplicationDelegate requires a var window: UIWindow? { get set } member, but in reality most application have a non-optional root window. It would be cool to have a property delegate that implements the optional delegating property with a non-optional value (potentially delayed).

1 Like

What would the setter for window do when it receives a nil value?