[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