[swift-evolution] [Review] SE-0030 Property Behaviors

Joe Groff jgroff at apple.com
Mon Feb 22 13:28:52 CST 2016


> On Feb 19, 2016, at 9:02 PM, Michel Fortin <michel.fortin at michelf.ca> wrote:
> 
> 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.

Yeah, I agree that meta-operations, when they exist, probably make sense as interfaces decoupled from any concrete implementation. Without that feature, though, behaviors as I've proposed are purely an implementation detail of the properties that use them. It makes sense to consider meta-operations as a separate feature for that reason.

> 
> 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.

Observers, like other behaviors, are allowing a property declaration to provide a block of code that fills in customization points in an otherwise pre-fab property implementation. From an implementation perspective that's not so different from a lazy property binding the initializer value.

> 
> 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.

That's an interesting approach. A similar effect can be achieved with protocols and protocol extensions alone, though. You could have a fully abstract behavior protocol "resetting" that declares the interface of a property with a `reset` metaoperation, and declare that as API, while satisfying the implementation with a refined protocol "resetToConstant" that provides the concrete implementation in an extension. I think that keeping the feature tied to protocols (sugared or not) keeps the model simpler. I also have trouble seeing how most behaviors would be useful as standalone types by themselves.

-Joe



More information about the swift-evolution mailing list