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

Michel Fortin michel.fortin at michelf.ca
Sat Feb 20 10:14:52 CST 2016


> Le 20 févr. 2016 à 1:16, Brent Royal-Gordon <brent at architechies.com> a écrit :
> 
>> 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
> 
> Some of the points in the "Using a protocol (formal or not) instead of a new declaration" alternative discussed in the proposal (https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md#using-a-protocol-formal-or-not-instead-of-a-new-declaration) are relevant to this suggestion too:
> 
>> 	• Behaviors would pollute the namespace, potentially with multiple global functions and/or types.

That's only a problem if those names are *only* useful as behaviors. This `DelayedStorage` struct is useful as a regular type too. The `Resettable` protocol can be implemented by types too. The way I'm defining things, many behaviors are going to be useful as a type, so it's logical that they use the same namespace.

In the end, I don't really mind if behaviors are in a different namespace or not. The separate namespace just felt unnecessary in my specific case.


>> 	• In practice, it would require every behavior to be implemented using a new (usually generic) type, which introduces runtime overhead for the type's metadata structures.

That might be a problem. Although I believe there is much less indirections with the way I propose things than by having a function that returns a type that conforms to a protocol like what was analyzed there. So I'm not sure how much of the analysis still applies.


>> 	• The property behavior logic ends up less clear, being encoded in unspecialized language constructs.

The same could be said about the current proposal: it attempts to unite all loosely related kinds of behaviors under a single unspecialized declaration syntax. We don't have one `type` declaration for types: instead we have `struct`, `class`, `enum`, and `protocol` which are different kinds of types with slightly different semantics.

With my proposal a property behavior behaves pretty much as a type, and will often be perfectly usable as a standalone type, so I felt things would end up being clearer as types rather than almost-type things. Not that I would really mind making them non-types on as separate namespace.


> In particular, note the point about runtime overhead. Behaviors are carefully designed to be basically equivalent to writing the same code yourself every time you need that semantic. If we weren't aiming for that, the design would be much simpler: we'd have an `init(initialValue:)` that could handle deferred initialization by using an `@autoclosure` and storing it in a property, for instance. But compared to writing the same thing yourself, that would waste memory on an extra property to store the closure, so we don't want to do that.

Which is why I added this optional `var` inheritance/conformance thing:

	struct Something<Value>: var {}

Here's how it works: a struct or protocol that conforms to `var` has access to the outer `context`, which is passed as a second hidden argument to all of the methods (second after `self`). A struct or protocol that inherits from `eager var` or `deferred var` has access to a `evalInitValue` closure provided as a third hidden argument. Syntactically, it just look like additional inherited members though.

A struct or protocol that does not inherit from `var` is just a regular struct or protocol: methods have no access to the outer context and the init value is only made available as an argument to the struct constructor.

Perhaps this mechanism is a bit too convoluted?

The only place I can find where this breaks is if the storage behavior needs some hooks. I had not realized this at first because hooks work really well with protocols used as behaviors, but structs cannot define abstract members. So that's a downside of this approach. It could be alleviated by making the struct declaration require that the inherited `var` implements a protocol:

	protocol DefaultValueProvider {
		associatedType Value
		var defaultValue: Value
	}
	struct Resettable<Value>: var<DefaultValueProvider> {
		var value: Value
		init() { value = defaultValue }
		func reset() { value = defaultValue }
	}

but I'm not sure how the underlying implementation would work here. It's also becoming a bit messy syntactically. That's perhaps a sign that we need a better dedicated construct to deal with this.



>> 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.
> 
> I think this is fraught with difficulties that will make it impractical at best.
> 
> Suppose you have a property `var foo: @Lazy Foo`, and you assign it to a variable like so: `let bar = foo`. What is the type of `bar`? Does `@Lazy` follow it somehow? What does it even mean if it does?

First, `@Lazy` cannot be used as a type modifier. It is defined as conforming to/inheriting from `deferred var` which means it applies only to `var`. That's needed for it to access `evalInitValue`. (You could redefine `Lazy` to store a closure, which is suboptimal in many ways but that would make it work as a type behavior. Let's assume this for the rest of the discussion.)

What is the type of `bar`. Quite simple, the type of bar is the type of `foo::value`. The general idea is that referencing to the property without the out-of-band member access operator (I'm using `::` for that) is equivalent to appending `::value`:

	foo = bar
	// is the same as
	foo = bar::value

If you used `Lazy<Foo>` instead of `@Lazy Foo`, then you'd have to add the `.value` yourself, and `bar = foo` would make `bar` of type `Lazy<Foo>` instead of `Foo`.


> Suppose you have an `Array<@DelayedStorage NSObject>` and a method which expects an `Array<NSObject>`. Can you pass the array to the method? Is this answer correct for all possible behaviors?

That's not possible because the memory layout is different. We have the same problem with protocols: you can't convert `Array<SomeObject>` to `Array<SomeProtocol>` in the general case where `SomeObject` conforms to `SomeProtocol`.


> Suppose you have an `Array<NSObject>` and a property which contains an `Array<@DelayedStorage NSObject>`. Can you assign the array to the property? Is this answer correct for all possible behaviors?

Again no. Same problem as above.


> Suppose you have an `Array<@DelayedStorage NSObject>`. What type should `Element` be in each of these Array members?

I get your point here. The type of `Element` has to be `NSObject`, not `@DelayedStorage NSObject` for all the functions and methods to work as intended. There's one way to "fix" this: parametrize the behavior type separately in the declaration of a generic:

	struct Array<@B T> { ... }

and here the array could reuse `@B T` for its storage, but just `T` elsewhere:

	var x: @B T // using the generic behavior
	var y: T // not using the generic behavior

That means that generics would need to opt-in in order to allow a type behavior in their type arguments, and then carefully decide where to use the behavior and when not to use it. I think that's an interesting concept, but it complicates things a bit more than I expected, perhaps to a point where it's not worth it.


> Ultimately, my sense is that trying to make behaviors-as-implicit-types in this fashion ends up with a lot of the problems of implicitly unwrapped optionals: essentially, it's hard to tell how far type inference will propagate the implicit types, or when the special implicit logic is invoked. We want less of that, not more.

Implicitly unwrapped optionals are indeed an apt comparison.


-- 
Michel Fortin
https://michelf.ca



More information about the swift-evolution mailing list