[swift-evolution] [Proposal] Property behaviors

Joe Groff jgroff at apple.com
Thu Jan 14 12:33:20 CST 2016


> On Jan 13, 2016, at 9:38 PM, Brent Royal-Gordon <brent at architechies.com> wrote:
> 
>>> 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?
>> 
>> You might want to constrain the behavior to apply only to some subset of a generic type:
>> 
>> var behavior optionalsOnly<Value>: Value? { ... }
>> var behavior dictionariesOfStringsOnly<Key>: Dictionary<Key, String> { ... }
> 
> Ah, so the type on the right side of the colon is the allowable variable type? That's a pretty handy feature. Could use a little more explanation, though. :^)
> 
>> I agree that an unconstrained type is likely to be the 95% case in practice. Maybe an approach more like what we do with generic extensions is appropriate; we could say that `Value` and `Self` are both implicit generic parameters, and use a freestanding `where` clause to constrain them.
> 
> No, your way is probably better. For instance, I think it's probably more useful for `optionalOnly`'s `Value` type to be the unwrapped type.
> 
>>> Should we actually force people to declare the `value` property?
>> 
>> That's a bit fiddly. `lazy` has optional storage, but produces a property of non-optional type, and other behaviors can reasonably have different (or no) storage needs. I feel like the storage should be explicitly declared.
> 
> That's fair.
> 
>>> While we're here, why does `initializer` have an explicit type? Can it be different from `Value`? What does it mean if it is?
>> 
>> As proposed, no, but it might be possible to relax that. My concern with `deferred initializer` floating on its own is that it looked weird. To my eyes, `initializer: T` makes it clear that something named `initializer` of type `T` is being bound in the behavior's scope.
> 
> It does look a little weird, but it has precedent in, for instance, `associativity` declarations.
> 
> I feel like there ought to be a way to build this part out of something that already exists. Perhaps a deferred initializer is just one with an autoclosure?
> 
> 	public var behavior resettable<Value>: Value {
> 	  initializer: Value
> 	  ...
> 	}
> 	
> 	var behavior lazy<Value>: Value {
> 	  initializer: @autoclosure Void -> Value
> 	  ...
> 	}
> 	
> 	public var behavior delayedMutable<Value>: Value {
> 	  // No initializer line, so it doesn't accept an initializer
> 	  ...
> 	}
> 
> Maybe it should be a `var` or `let` property, and it just has a specific name. That might be a little odd, though.

@autoclosure instead of `deferred` is an interesting idea. Also, as I mentioned to Felix, if we didn't have a way to bind the initializer, the same functionality could be achieved using custom accessors, at the loss of some sugar. If that's acceptable, it would greatly simplify the proposal to leave out the ability to hijack the initializer.

> 
>>> 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?
>> 
>> The base property is out of the current behavior implementation's control, since it could be a `super` property or the result of another behavior's instantiation. In the `super` case, the `base` property's storage won't be initialized until later when super.init is invoked, and in the behavior composition case, the `base` property's initialization is in the prior behavior's hands.
> 
> Ooh, that's a good point about overrides.
> 
> Do you think it makes sense to allow non-base properties to accept initializers? If so, what does it mean? What happens if one behavior wants deferred and another wants eager?

I think, fundamentally, only the innermost behavior can control initialization. If you innermost behavior itself has a base, then there's essentially an implicit "stored" behavior nested within it, which has the standard stored property initialization behavior.

> 
>> Capture is tricky; thanks for bringing it up, since I hadn't thought of it. As I see it being implemented: in a mutating context, `self` is inout, and the behavior's storage always has to be implicitly projected from `self` in order to avoid overlapping `inout` references to self and the behavior storage. Closures would have to capture a shadow copy of `self`, giving the often unexpected behavior you see with captured `inout` parameters today losing attachment to their original argument after the `inout` parameter returns. In nonmutating contexts, everything's immutable, so `dispatch_async` can safely capture the minimal state referenced from within a `get`.
> 
> That's not the greatest behavior, but it's probably the best you can do.
> 
> If Self is a reference type, will capturing the behavior hold a strong reference to `self`? I assume that in that scenario, it will, and then assigning to the behavior's properties *will* work properly.

If Self is class-constrained, we could always strongly capture it. Otherwise, we'd conservatively only be able to do so in nonmutating contexts.

> 
>>> 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()
>>> 	}
>> 
>> That's an interesting idea; I think we could add something like that later if it's useful. It's my intent in this proposal to avoid treating behaviors as first-class types and keep them mostly instantiation-based, in order to avoid the metadata instantiation overhead of a type-based approach. That would mean that `bar.lazy` isn't really a first-class entity.
> 
> Fair enough. It's certainly not so critical that we need to stop the show until we have it.
> 
> One more thing: should there be a way to pass arbitrary data, rather than initializers or accessors, into a behavior? For instance, it would be nice if you could do something like:
> 
> 	var behavior backedByJSON<Value: JSONRepresentable where Self: JSONObjectRepresentable>: Value {
> 		var key: String
> 		
> 		init(key: String) {
> 			backedByJSON.key = key
> 		}
> 		
> 		get {
> 			return Value(JSON: self.JSON[key])
> 		}
> 		set {
> 			self.JSON[key] = newValue.JSON
> 		}
> 	}
> 	
> 	struct User: JSONObjectRepresentable {
> 		var JSON: [String: JSONType]
> 		
> 		var [backedByJSON(key: "id")] ID: Int
> 		var [backedByJSON(key: "name")] name: String
> 		
> 		var [backedByJSON(key: "posts")] posts: [Post]
> 	}


That parameterization could be achieved, somewhat more verbosely, using an accessor 'key { return "id" }'. If behaviors could be parameterized in-line, I wouldn't do it by passing the params to `init`, since that again imposes the need for per-instance storage to carry `key` from the behavior initialization to the property implementation.

-Joe




More information about the swift-evolution mailing list