[swift-evolution] [Proposal] Property behaviors

Brent Royal-Gordon brent at architechies.com
Wed Jan 13 23:38:36 CST 2016


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

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

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

>> 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]
	}

-- 
Brent Royal-Gordon
Architechies



More information about the swift-evolution mailing list