[swift-evolution] [Review] SE-0030 Property Behaviors
Michel Fortin
michel.fortin at michelf.ca
Fri Feb 19 23:02:26 CST 2016
Le 15 févr. 2016 à 17:55, Joe Groff <jgroff at apple.com> a écrit :
>
> I think the whole space of out-of-band meta-operations deserves its own separate discussion. In addition to your concerns, exposing metaoperations as interface also introduces bigger concerns about resilience, protocol conformance, etc. that deserve deeper consideration.
The way I see it, those meta-operations are just functions enclosed inside the variable's namespace. So in a protocol, and from an API point of view, they'd look like this:
protocol MyCounter {
var count: Int {
get
set
func reset()
}
}
Here, you don't need to know that `reset` is provided by a particular behavior. That is just an implementation detail.
I find your behavior proposal lacking in that regard. A behavior contains both the interface and the implementation of those meta-operations. It shouldn't. If you want to implement @resettable differently for one of the variables, that should not be visible to the outside world. The default implementation of @resettable shouldn't be the only one allowed.
It's interesting that your current implementation uses a protocol with a protocol extension to define behaviors, because the more I think about behaviors the more I believe they should be like a protocol and provide only the interface. The above could be reformulated like this:
protocol Resettable {
func reset()
}
protocol MyCounter {
@Resettable var count: Int { get set }
}
> As I noted in your pre-review comments, trying to separate storage control from observers doesn't seem like a net simplification to me. If you try to extend either feature on its own, you end up encroaching in the other's space. A behavior that manipulates storage may need per-declaration hooks, which accessor provides. An accessor may need to introduce bookkeeping storage.
An "accessor hook" is not at all the same thing as an "observer" like `willSet` in my mind. A hook from a behavior is more like requirement of something that must be provided. You are proposing that this requirement be expressed using a syntax similar observers like `willSet`, but I'm not sure reusing the same syntax it's the right thing to do.
If a behavior was a protocol, it could do what a protocol is good at: define requirements (hooks) and leave those requirements to be implemented by the property definition. You could then implement them in the property definition itself:
protocol Resettable {
func reset()
}
@Resettable var count: Int {
func reset() { count = 0 }
}
And if a behavior was a protocol, then you could also provide a default implemenation as a protocol extension. Here I'll make the protocol conform to a magic `eager var` protocol that makes available the init value for protocol extensions:
protocol Resettable: eager var {
func reset()
}
extension Resettable {
// `value` accessible because this protocol inherits from `var`
// `evalInitValue` accessible here because this protocol inherits from `eager var`
func reset() { value = evalInitValue() }
}
@Resettable var count: Int
And you could allow the behavior to be something else than a protocol too. For instance, allow it to be a struct if you need to implement your own storage:
struct DelayedStorage<Value> {
var storage: Value?
var value: Value {
get {
guard let theValue = storage else {
fatalError("delayedImmutable property read before initialization")
}
return theValue
}
set {
guard storage == nil else {
fatalError("delayedImmutable property rewritten after initialization")
}
storage = newValue
}
}
init() {
storage = nil
}
}
@DelayedStorage var outlet: UIView
And if you need access to the initial value within the struct, you can make the struct inherit from `deferred var` or `eager var` which would make the initial value accessible as an inherited member:
struct Lazy<Value>: deferred var {
var storage: Value?
var value: Value {
mutating get {
guard let theValue = storage else {
// evalInitValue accessible because this struct inherits from `eager var`
storage = evalInitValue()
}
return theValue
}
set {
storage = newValue
}
}
// does not allow initialization with a value (no constructor for it)
init() {
storage = nil
}
}
@Lazy var processorCount: Int = NSProcessInfo.processInfo().processorCount
If a struct or a protocol inherit from `var`, then its methods have access to the outer context (the scope the var lives in if it's a struct or a class). That scope would be exposed through the `context` member also inherited from `var`. And to better make use of it, you should also be able to constrain the context the `var` lives in:
protocol Synchronizable {
func withLock<T>(@noescape task: () -> T) -> T
}
struct Synchronized<Value>: var in Synchronizable {
var storage: Value
var value: Value {
get {
context.withLock {
return storage
}
}
set {
context.withLock {
storage = newValue
}
}
}
init(_ initValue: Value) {
storage = initValue
}
}
@Synchronized var faultCount: Int = 0
Using a mix of protocols and structs for behaviors allows protocols to define the interface while a struct and/or extensions and/or the property declaration can provide the implementation. All this gives a lot of flexibility in how to implement what is exposed in the interface.
So, to summarize, that would decompose property behaviors in a couple of smaller features:
1. The ability to use a struct as a wrapper for a property's value: the `value` property of the struct becomes the property's value and other members are accessible using an out-of-band member access operator.
2. The ability for a property to define out-of-band members (`var`s `let`s, and `func`s) inline inside the property declaration (like a custom `reset` function) that can be accessed using the out-of-band member access operator.
3. The ability to have a property conform to a protocol that either the storage struct, a protocol extension, or the inline out-of-band members defined in the property must implement.
4. The ability for a protocol or struct to inherit from `var`, which would add the `value` and `context` members. The `evalInitValue` member would only be available for `deferred var` or `eager var`, and in the case of `deferred var` not available in the constructor (because it might require `self` from the outer context).
5. Observers, like `willSet`, `willChange`, etc. can be implemented as a separate `observer` construct that can be used on any property simply by writing the observer inside the property declaration. (As discussed in my previous email.)
There's something else interesting with this type-based approach. Take note of how the `DelayedStorage` struct above does not inherit from `var`. That's because DelayedStorage does not need access to the outer context or `evalInitValue()`. And this makes `DelayedStorage` a perfectly valid struct to use elsewhere, such as:
var delayedObjects: [DelayedStorage<NSObject>]
You could implement Atomic, NSCopying, and many other behaviors this way and be able to reuse them elsewhere.
And, just teasing... but maybe there's a way a behavior could be attached as a type modifier instead of a property modifier. This would allow you to write `var delayedObjects: [@DelayedStorage NSObject]` and not have to access the value by appending `.value` every time.
--
Michel Fortin
https://michelf.ca
More information about the swift-evolution
mailing list