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.
Unsurprisingly, there's conflicting signal here, but I am certainly inclined to limit additional growth of the proposal. We have quite a pile of nifty examples to start with, and much to learn about this feature. The implementation is itself close to ready.
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
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 ofstorageValue
if it's important. We could make the backing storage a private var$$foo
;)
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.
Actually, now that I think about it, a much simpler model overall would be:
-
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)
-
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.
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.
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).
Or did you mean there's a collision in a more general sense, as in a
_
could mean either a user-declared variable or a compiler-synthesized variable?
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.
(Also, what's the first reason not to use an underscore? )
It's just the ugly look ;-)
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 APIsprivate
(and so on for other access modifiers).
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.
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.
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 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 toinit(storageValue:)
. This initializer would support out of line initialization via the storage value. It wouldnât cover every case (including yourBox
example) but it would be better than not supporting out of line initialization when thestorageValue
is different than the delegate itself.
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.
Iâm also curious why you chose to expose both
Box
andRef
in the example. Itâs possible to do something similar that only exposes a singleRef
property delegate that uses itself as the type ofstorageValue
:
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
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)
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.
@Douglas_Gregor any feedback regarding a different name for
storageValue
? I really think this property should be calleddelegateValue
as it resembles more its semantics.
I agree that delegateValue
is a better name.
Doug
@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?
@Douglas_Gregor can you clarify how
delegateValue
actually works? Why does the compiler know that$property
should be seen asdelegateValue
?
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 }
}
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?
That's correct.
Doug
It's essentially the same thing as with
value
: the real storage property gets a truly-hidden name (heh, we could call it$property
)
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).
For example
UIApplicationDelegate
requires avar 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).
What would the setter for window
do when it receives a nil
value?