[Pitch #2] Property Delegates by Custom Attributes

I was thinking the former actually. The $foo identifier would not be available. You make a good point about out of line initialization though. Isn't that already broken when the storage value is a different type than the property delegate, as in your Box / Ref example though? I wonder if there is another way to solve out of line initialization of the delegate itself.

While I don't love the var, I do really like the idea of the property delegate deciding its storage synthesis settings / access level across all of it's usages. I'm pretty sure that with all of the motivating examples the storage would be consistently private/internal/public across everywhere it's used.

Maybe instead of doing it at the call site like:
private(storage) public @Lazy var prop

we define it with the @propertyDelegate, i.e:
@propertyDelegate(storage:[.fileprivate])

and then all users of the property delegate synthesize a fileprivate storage.

I wasn't talking about anything to do with access control. I was talking about not synthesizing the $foo property at all unless a property delegate opts-in by implementing storageValue. When this property is synthesized the user of the property delegate should have control of its visibility. There will be cases where the API of the property delegate is either not used or only used in the implementation of the type that declaring the property backed by the delegate.

Sure, but talking about whether to synthesize or not being a decision of the property delegate implementation makes me think it's worth also defining the access level of said synthesized storage in the property delegate implementation as well.

i.e. instead of deciding to synthesize based on the existence of the storageValue var, we decide whether to synthesize the storage and specify its property using an enum:

enum Storage {
  case `public` // storage synthesized and public
  case `fileprivate` // storage synthesized and fileprivate
  case none // storage not synthesized
}

and the that value gets used when declaring the delegate:

@propertyDelegate(storage: .none)

Edit: I'm thinking of this as similar to "scopes" in the Static Custom Attributes pitch

This doesn't make sense to me. Having a useful API (or not) beyond exposing a value is a decision made by the property delegate. How broadly visible that API should be is a concern of the client code.

3 Likes

It sounds like instead of .none, the .private case should behave exactly the same and the name is more consistent to the language. So:

enum Storage {
  case `public` // storage synthesized and public
  case `fileprivate` // storage synthesized and fileprivate
  case `private` // storage not synthesized
}

but personally I would rather still keep it on the caller side which gives more flexibility and with the mix of your proposition how to describe it:

public @Lazy(storage=private) var prop
private(storage) public @Lazy var prop

This nation even though fits to the Swift "style" in my opinion has to downsides:

  1. It will be confusing according to the order of access level according to the property and the storage all the time for developers, so we might try to write:
  2. It is a bit hard to grasp by the human at which access level at I'm trying to focus on especially in a case:
    • public public(storage) @Lazy var prop
    • public(storage) public @Lazy var prop

You'd effectively have to initialize the Box in the initial declaration, and the author of the property delegate type could offer to allow additional setup via the type of storageValue if it's important. We could make the backing storage a private var $$foo ;)

Doug

Yeah, it's a mistake. I'm fixing both compiler and the proposal now. Thank you!

Doug

1 Like

Hi all. We seem to have converged on most aspects of this proposal, so I'll ask the Core Team about scheduling a review. The implementation should be enough to kick the tires with the Ubuntu 16.04 Linux toolchain or macOS toolchain, although there are two annoying limitations:

  • init(initialValue:) that takes an @autoclosure is currently broken

  • Property delegates cannot be used on local variables; only properties of types work well right now (it lazy is similarly broken and always has been!)

    Doug

10 Likes

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)