[swift-evolution] [Proposal] Property behaviors
Brent Royal-Gordon
brent at architechies.com
Wed Jan 13 19:04:07 CST 2016
> 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 {}
> 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
}
}
While we're here, why does `initializer` have an explicit type? Can it be different from `Value`? What does it mean if it is?
> 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?
> 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`?
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()
}
> 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.
> 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
}
}
> 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.
--
Brent Royal-Gordon
Architechies
More information about the swift-evolution
mailing list