[swift-evolution] [Proposal] Property behaviors
Joe Groff
jgroff at apple.com
Wed Jan 13 20:02:18 CST 2016
> On Jan 13, 2016, at 5:04 PM, Brent Royal-Gordon <brent at architechies.com> wrote:
>
>> Thanks everyone for the first round of feedback on my behaviors proposal. I've revised it with the following changes:
>>
>> - Instead of relying on mapping behaviors to function or type member lookup, I've introduced a new purpose-built 'var behavior' declaration, which declares the accessor and initializer requirements and provides the storage and behavior methods of the property. I think this gives a clearer design for authoring behaviors, and allows for a more efficient and flexible implementation model.
>> - I've backed off from trying to include 'let' behaviors. As many of you noted, it's better to tackle immutable computed properties more holistically than to try to backdoor them in.
>> - I suggest changing the declaration syntax to use a behavior to square brackets—'var [behavior] foo'—which avoids ambiguity with destructuring 'var' bindings, and also works with future candidates for behavior decoration, particularly `subscript`.
>
> This iteration is a big improvement; the use of more syntax is a definite help. Some specific comments:
>
>> var behavior lazy<Value>: Value {
>
> I'm somewhat concerned that this looks like you're declaring a variable called "behavior", but I do like that it keeps the opportunity open for us to add, say, `let` behaviors or `func` behaviors in a later version of Swift. `behavior var` doesn't read very fluidly, but it doesn't mislead you, either.
>
> Also, why the `: Value`? Can that ever be anything but `: Value`? What does it mean if it is?
>
> Also also, why the `<Value>`? There will always be exactly one generic type there, right? I get that you can dangle requirements off of it, but can't you do that by supporting a `where` clause?
>
> var behavior lazy {}
> var behavior NSCopying where Value: NSCopying {}
You might want to constrain the behavior to apply only to some subset of a generic type:
var behavior optionalsOnly<Value>: Value? { ... }
var behavior dictionariesOfStringsOnly<Key>: Dictionary<Key, String> { ... }
I agree that an unconstrained type is likely to be the 95% case in practice. Maybe an approach more like what we do with generic extensions is appropriate; we could say that `Value` and `Self` are both implicit generic parameters, and use a freestanding `where` clause to constrain them.
>> private var value: Value? = nil
>
> Should we actually force people to declare the `value` property? I can't think of many behaviors which won't need one. You can put the `base` directly on the behavior declaration (arguably it belongs there since the presence or absence of a base is definitely *not* an implementation detail), and if `: Value` is currently purposeless, well, now we have a reason for it: to declare the `value` property's type.
>
> var behavior lazy: Value? {
> deferred initializer: Value
> get {
> if let value = value {
> return value
> }
>
> let initialValue = initializer
> value = initialValue
> return initialValue
> }
> set {
> value = newValue
> }
> }
That's a bit fiddly. `lazy` has optional storage, but produces a property of non-optional type, and other behaviors can reasonably have different (or no) storage needs. I feel like the storage should be explicitly declared.
>
> While we're here, why does `initializer` have an explicit type? Can it be different from `Value`? What does it mean if it is?
As proposed, no, but it might be possible to relax that. My concern with `deferred initializer` floating on its own is that it looked weird. To my eyes, `initializer: T` makes it clear that something named `initializer` of type `T` is being bound in the behavior's scope.
>
>> Neither inline initializers nor init declaration bodies may reference self, since they will be executed during the initializing of a property's containing value.
>
> This makes sense, but is there some opportunity for a second-phase initialization that runs when `self` is accessible? I can imagine behaviors using this to register themselves using a method provided by a protocol on `Self`. (Although perhaps it would be better to focus on exposing behaviors through reflection.)
>
>> For the same reason, an init also may not refer to the base property, if any
>
>
> Why not? I would assume that behaviors are initialized from the inside out, and an eager-initialized property will need to initialize its base property's value. Or is there an implicit rule here that a behavior with a base property cannot take an initializer?
The base property is out of the current behavior implementation's control, since it could be a `super` property or the result of another behavior's instantiation. In the `super` case, the `base` property's storage won't be initialized until later when super.init is invoked, and in the behavior composition case, the `base` property's initialization is in the prior behavior's hands.
>
>> Inside a behavior declaration, self is implicitly bound to the value that contains the property instantiated using this behavior. For a freestanding property at global or local scope, this will be the empty tuple (), and for a static or class property, this will be the metatype. Within the behavior declaration, the type of self is abstract and represented by the implicit generic type parameter Self. Constraints can be placed on Self in the generic signature of the behavior, to make protocol members available on self:
>
>
> This is handy, but I wonder if it's a good idea to make `self` refer to something else and then have an apparently anonymous context for all the variables and methods. It just has a really strange feel to it. What aspects of the behavior are captured by a closure? Can you safely dispatch_async() inside a `get`?
Capture is tricky; thanks for bringing it up, since I hadn't thought of it. As I see it being implemented: in a mutating context, `self` is inout, and the behavior's storage always has to be implicitly projected from `self` in order to avoid overlapping `inout` references to self and the behavior storage. Closures would have to capture a shadow copy of `self`, giving the often unexpected behavior you see with captured `inout` parameters today losing attachment to their original argument after the `inout` parameter returns. In nonmutating contexts, everything's immutable, so `dispatch_async` can safely capture the minimal state referenced from within a `get`.
> Relatedly: How first-class are behaviors? Can you assign `foo.bar.lazy` to a variable, or pass it as a parameter? Or can you pass a value along with its behavior?
>
> func consumeThing(inout thing: [resettable] Thing) {
> thing.use()
> thing.resettable.reset()
> }
That's an interesting idea; I think we could add something like that later if it's useful. It's my intent in this proposal to avoid treating behaviors as first-class types and keep them mostly instantiation-based, in order to avoid the metadata instantiation overhead of a type-based approach. That would mean that `bar.lazy` isn't really a first-class entity.
>> Behaviors are private by default, unless declared with a higher visibility.
>
>
> I think this is the right move 90% of the time, but I also think that there are certain behaviors which should be public (or as-visible-as-the-property) by default. For instance, consider this one:
>
> public var behavior observable<Value> {
> base var value: Value
>
> var observers: [ObserverIdentifier: (instance: Self, oldValue: Value, newValue: Value) -> Void]
>
> public func addObserver(observer: (instance: Self, oldValue: Value, newValue: Value) -> Void) -> ObserverIdentifier {
> let identifier = ObserverIdentifier()
> observers[identifier] = observer
> return identifier
> }
> public func removeObserverWithIdentifier(identifier: ObserverIdentifier) {
> observers[identifier] = nil
> }
>
> get { return value }
> set {
> let oldValue = value
> value = newValue
> for observer in observers.values {
> observer(self, oldValue, newValue)
> }
> }
> }
>
> It's very likely that a public property on a public type will want this behavior to be public too. On the other hand, it's also a relatively niche case and might not be worth complicating the model.
I agree. It might be interesting for behaviors to specify their preferred visibility policy.
>
>> Behavior extensions can however constrain the generic parameters of the behavior, including Self, to make the members available only on a subset of property and container types.
>
> It might also be useful to allow extensions to provide constrained default accessor implementations:
>
> public var behavior changeObserved<Value>: Value {
> base var value: Value
>
> accessor detectChange(oldValue: Value) -> Bool
> mutating accessor didChange(oldValue: Value) { }
>
> get {
> return value
> }
> set {
> let oldValue = value
> value = newValue
> if detectChange(oldValue) {
> didChange(oldValue)
> }
> }
> }
>
> extension changeObserved where Value: Equatable {
> accessor detectChange(oldValue: Value) -> Bool {
> return value != oldValue
> }
> }
Interesting point. You can do that with protocol extensions.
>
>> The proposal suggests x.behaviorName for accessing the underlying backing property of var (behaviorName) x. The main disadvantage of this is that it complicates name lookup, which must be aware of the behavior in order to resolve the name, and is potentially ambiguous, since the behavior name could of course also be the name of a member of the property's type.
>
>
> I suggest using `foo.bar[lazy]` as the property access syntax. This echoes the use of square brackets to declare properties, is less likely to look like it conflicts with a valid operation (most types have properties, but very few have subscriptors), and should never be ambiguous to the compiler unless you decide to make a variable with the name of a behavior and then subscript a data structure with it.
>
> If you cared more about ambiguity than appearance, you could even reverse the order and make it `type.[lazy]bar`, but good lord is that ugly.
Yeah, I got a similar suggestion from Patrick Smith on Twitter. I kinda like it; it echoes the declaration syntax.
-Joe
More information about the swift-evolution
mailing list