[swift-evolution] [Proposal] Property behaviors

Joe Groff jgroff at apple.com
Thu Jan 21 20:36:39 CST 2016


> On Jan 21, 2016, at 10:16 AM, Michel Fortin <michel.fortin at michelf.ca> wrote:
> 
> Le 20 janv. 2016 à 21:44, Joe Groff <jgroff at apple.com> a écrit :
>> 
>>> On Jan 20, 2016, at 6:12 PM, Michel Fortin <michel.fortin at michelf.ca> wrote:
>>> 
>>> Le 19 janv. 2016 à 21:38, John McCall via swift-evolution <swift-evolution at swift.org> a écrit :
>>> 
>>>> One of my worries about this proposal in general is that you’ve listed out half-a-dozen different uses and every single one seems to require a new twist on the core semantics.
>>> 
>>> That's my general feeling too about this proposal. I just didn't know how to express what you said above.
>> 
>> I *did* somewhat strategically pick my examples to try to cover the breadth of different things I see someone wanting to do with this feature.
>> 
>>> 
>>> To me this proposal feels like it's is about trying to find a solution to multiple problems at once. A new problem arise that looks like it could be solved by a behavior, so the behavior feature expands to accommodate it. It looks like the wrong approach to me. 
>>> 
>>> The correct approach in my opinion would be to try to make various parts of this proposal standalone, and allow them to combine when it makes sense. For instance, if you wanted to define a standalone feature for defining custom accessors that can be used everywhere, you wouldn't come with something that requires a behavior annotation at the variable declaration. You'll come with something simpler that might looks like this:
>>> 
>>> custom_acccessor willSet<T>(newValue: T) { // define a custom accessor...
>>> 	set { // ... by redefining the setter...
>>> 		willSet(newValue) // ...inserting a call to the accessor here...
>>> 		currentValue = newValue // ...before calling the underlying setter
>>> 	}
>>> }
>>> custom_acccessor didSet<T>(oldValue: T) {
>>> 	set {
>>> 		let oldValue = currentValue
>>> 		currentValue = newValue
>>> 		didSet(oldValue)
>>> 	}
>>> }
>>> custom_acccessor willChange<T>(newValue: T) {
>>> 	willSet {
>>> 		if currentValue != newValue {
>>> 			willChange(newValue)
>>> 		}
>>> 	}
>>> }
>>> 
>>> Then at the declaration point you just directly use the globally accessible accessor:
>>> 
>>> 	var myvar: Int {
>>> 		willChange { print("will change to \(newValue)") }
>>> 	}
>>> 
>>> This fulfills at least one of the use cases. Can't we do the same treatment to each proposed use cases and see if there are other parts that can stand on their own?
>> 
>> I considered this approach. It works for behaviors that don't need to control a property's storage and only change the property's access behavior. To be fair, that covers a lot of ground, including things like observing, NSCopying, resetting, and locking synchronization. We would still need a feature, which could certainly be a different one, to generalize annotations that control the storage policy for decorated properties, which could cover things like laziness, indirect storage, unowned/weak-ness, dirty-tracking, C-style atomics, and pointer addressability—basically, anything where a plain old stored property of the API type isn't sufficient. (You could even throw get/set in this bucket, if you wanted to be super reductionist.) Finally, there's the feature to add operations on a *property* independent of its *type*, which interacts usefully with both other features—you need a way to reset a resettable or lazy property; maybe you want to bypass a synchronized property's lock in one place, etc. We'd like to improve on the "classic" answer of exposing an underlying ivar or property in these cases.
>> 
>> If you want to break it down in micro-features, I guess there are three here:
>> 
>> 1. Factoring out storage patterns,
>> 2. Factoring out accessor patterns, and
>> 3. Adding per-property operations.
>> 
>> (1) tends to be tightly coupled with (2)—if you're controlling storage, you almost certainly want to control the accessors over that storage. And (3) is useful with both (1) and (2). If there are separate features to be factored out here, I think they're very entangled features.
> 
> No single language feature necessarily has to "control" the accessor. The accessor is really of a pile of "custom accessor code" wrapping one another, some in the variable declaration, some in the variable's behaviours, and so why not some others in a global accessor declaration? They just pile up on top of each other.
> 
> Last post was about (2), so let's try to attack (1) using this approach. Basically, this is going to be the same thing as your behaviour proposal, minus `var` inside of it, minus custom accessors defined inside of it, minus functions inside of it (we'll revisit that at the end), and where the base property is implicit (`currentValue`) and the initializer is implicit too (`initValue`).
> 
> I'll start with an "identity" behavior, a behavior that wraps a base property while actually doing nothing:
> 
> // identity for type T creates storage of type T
> storage_behavior identity<T>: T {
> 	// regular initializer, works with eager or deferred initialization
> 	init {
> 		// initValue is implicitly defined in this scope
> 		currentValue = initValue // initializing the storage
> 	}
> 	get {
> 		return currentValue
> 	}
> 	set {
> 		currentValue = newValue
> 	}
> }
> 
> Sometime you need to force the initializer to be eager or deferred. So we can do that with a keyword, in which case for `eager` the `initValue` becomes available anywhere inside the behavior, and for `deferred` it becomes available everywhere outside of `init`:
> 
> // identity for type T creates storage of type T
> eager storage_behavior eagerIdentity<T>: T {
> 	// initValue is implicitly defined in this scope
> 
> 	// eager initializer (takes not argument)
> 	init {
> 		currentValue = initValue // initializing the storage
> 	}
> 	get {
> 		return currentValue
> 	}
> 	set {
> 		currentValue = newValue
> 	}
> }
> 
> Note at this point how this is basically the same syntax as with the `custom_accessor` examples from my last post, minus there is no accessor to call in `get` or `set`, plus you have the ability to affect the storage. The main difference for the user is that, since it's a behavior, you opt in to it by annotating the variable. Whereas in the case of `custom_accessor` you opt it by writing the custom accessor body inside of the variable's accessors.
> 
> Ok, now let's write lazy:
> 
> // lazy for type T creates storage of type T?
> deferred storage_behavior lazy<T>: T? {
> 	init { 
> 		// defered initValue not available in this scope
> 		currentValue = nil // initializing the storage
> 	}
> 	get {
> 		if currentValue == nil {
> 			currentValue = deferredInitValue
> 		}
> 		return currentValue! // converting from T? to T
> 	}
> 	set {
> 		currentValue = newValue // converting from T to T?
> 	}
> }
> 
> Now let's make it atomic:
> 
> storage_behavior atomic<T>: Atomic<T> {
> 	init {
> 		currentValue = Atomic(initValue)
> 	}
> 	get {
> 		return currentValue.payload
> 	}
> 	set {
> 		currentValue.payload = newValue
> 	}
> }
> 
> Note how I can avoid adding variables inside the behavior by just changing the underlying type to be some kind of wrapper of the original type. That makes the feature simpler.
> 
> We can also write synchronized (for variables in a context where self is Synchronizable):
> 
> storage_behavior synchronzied<T where Self == Synchronizable>: T {
> 	init {
> 		currentValue = initValue
> 	}
> 	get {
> 		return self.withLock {
> 			return currentValue
> 		}
> 	}
> 	set {
> 		self.withLock {
> 			currentValue = newValue
> 		}
> 	}
> }
> 
> Here is a minimalistic logging behavior:
> 
> storage_behavior logging<T>: T {
> 	// implied "identity" version of init, get, and set.
> 	willChange {
> 		print("\(currentValue) will change for \(newValue)")
> 	}
> }
> 
> This last example is interesting: it shows that you can use a globally-defined accessor inside of the behavior; you don't have to define a `set` or a `get` to make it useful. This logging behavior is basically just a shortcut for defining a variable like this:
> 
> 	var myvar: Int {
> 		willChange {
> 			print("\(myvar) will change for \(newValue)")
> 		}
> 	}
> 
> 	@logging var myvar2: Int // way less boilerplate!
> 
> So in the end, a storage behavior model defined like this brings to the table the `init` and the modified storage type. Otherwise it's just a nice and convenient way to avoid repeating boilerplate you can already write inside of a variable declaration. Which is great, because with less special rules the whole behaviour feature looks much more approachable now.

You're glossing over some of the subtleties of initialization. Recall that a "plain old" stored property can be initialized out-of-line:

func foo() {
  var x: Int
  ...
  x = 1
}

struct Bar {
  var y: Int

  init(y: Int) {
    self.y = y
  }
}

which creates complications for your 'init { }' model. If initializers chain the way you describe, then either storage_behaviors can never be initialized out-of-line, which would be a regression for didSet/willSet applications, or we need to gain the ability for definite initialization to turn 'x = 1' into an 'init' call. However, since storage_behavior can refer to its initializer anywhere, not only during initialization, the initialization assignment potentially has to *capture* the RHS value to be evaluatable later, which would be surprising. If a behavior wants to be able to override a superclass property, as didSet/willSet can do today, then initialization is out of the behavior's control. These are the factors that influenced the "base property" design in my proposal—if you want behaviors that compose, they really *can't* meddle in the underlying base property's initialization. These use cases can be addressed instead by your accessor modifier mechanism; however, if you try to break apart storage behaviors and custom accessors, then both features suffer—storage behaviors can't require accessors to parameterize behavior, and custom accessors can't introduce new storage if needed to apply their implementation over the underlying property.

-Joe
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160121/9edbdc35/attachment.html>


More information about the swift-evolution mailing list