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

Brent Royal-Gordon brent at architechies.com
Fri Feb 19 02:43:39 CST 2016


> Based on review feedback, I've revised the declaration syntax proposal for property behaviors to be more in line with our other declaration forms, reverting to the earlier pre-review "var behavior" proposal. I've updated the proposal in swift-evolution:
> 
> https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md
> 
> In discussion with the core team, we've also strongly come in favor of applying behaviors to properties using attribute syntax, e.g.:
> 
> @lazy var x = 111
> @delayed var x: Int
> 
> They're definitely attribute-like, and we think it makes sense for behaviors to be the first of hopefully many kinds of user-defined behaviors. What do you all think of this direction?

I like all of this overall. Discussion in a few parts below.

***

I'm fine with the new declaration syntax. I thought the declaration-follows-use syntax was unusually well suited to this feature since it more or less acts as a template for code surrounding a property, but there's nothing wrong with this new alternative.

I think the one part I don't really like is the first three words. It's a little odd having so many lowercase keywords in a row without any punctuation; I'd prefer a syntax with more texture. More worryingly, though, it appears that Apple frameworks have at least four public classes with properties called `behavior`, including `NSPopover`; these could be mistaken for behavior declarations. I see a few potential solutions here:

1. Disambiguate on the absence of punctuation; `var behavior` without a `:` or `=` after it can't be a property, so it must be a variable behavior. This seems mistake-prone, though:
	var behavior NSPopoverBehavior
	
2. Require backticks if you're declaring a property. This is the simplest solution, but kind of ugly.
	var `behavior`: NSPopoverBehavior

3. Reverse the order of the keywords, yielding `behavior var`. This avoids ambiguity, but it doesn't read as well:
	behavior var lazy<Value>: Value

4. Require the usage punctuation, whatever it is, at the declaration site. (I'll assume `@`.) `:` or `=` is a property named "behavior"; `@` is a property behavior; anything else is an error. This also gives the declaration some of that missing texture I was complaining about.
	var behavior @lazy<Value>: Value

(Technically, the `behavior` keyword is redundant in option 4, but I think it's valuable as something you can search for when you see `var @lazy` and have no idea what that means.)

***

One thing I don't like about this new syntax is the standalone `initialValue` keyword (nor the similar `name` keyword in Future Directions, which has a type attached for additional weirdness). I'm wondering if instead, we can have the user declare an accessor named `initialValue`:

	var behavior lazy<Value>: Value {
		accessor initialValue() -> Value
		
		private var value: Value?
		
		mutating get {
			value ??= initialValue()	// Borrowing syntax from SE-0024: Optional Value Setter
			return value!
		}
	}

This turns the computed property into a function, but I don't think that's necessarily a bad thing. But if you want it to remain a computed property, I do have another option for you.

Suppose we allow accessors to be either methods *or* readonly computed properties. In the past, we've discussed parameterizing behaviors; this would be a way to do it. For example:

	var behavior json<Value: JSONRepresentable where Self: JSONObject>: Value {
		// Accessor methods now carry an additional `func` keyword:
		accessor func toJSON(value: Value) -> JSONValue {
			return value.json
		}
		accessor func fromJSON(json: JSONValue) -> Value {
			return Value(json: json as! Value.JSONRepresentation)
		}
		
		// And we can now say:
		
		accessor var key: String
		
		// `key` is a readonly computed property which is bound to the expression provided 
		// when the behavior is applied to the property.
		// 
		// Just as you can provide an accessor method with a default implementation, so you 
		// can provide an accessor property with a default value. For instance, if `#name` were
		// the syntax to access the property name, the above could be:
		// 
		//	accessor var key = #name
		
		get {
			return fromJSON(self.json[key])
		}
		set {
			self.json[key] = toJSON(newValue)
		}
	}
	
	struct Person: JSONObject {
		var json: [String: JSONValue]
		
		// I have two different ideas for the syntax at the application site.
		
		// Option 1:
		@json var identifier: String { key = "id" }
		
		// Option 2:
		@json(key="id") var identifier: String

		// Note: The `=` in Option 2 would become a `:` if Erica Sadun's draft proposal on using 
		// colons in attributes is accepted.
	}

(If we take option 2, there are all sorts of things we can consider doing to bring behavior accessor vars in line with the things we currently see in attribute parameters. For instance, we could allow boolean accessor vars to be set by the presence or absence of their name in the list, without a `true` value being included. We might also allow you to give an accessor var an external name of _ to indicate that no label is needed. If we make this sophisticated enough, we might be able to turn many attributes into empty behaviors that the compiler knows are magic. For instance, if we're willing to turn @objc(foo) into @objc("foo"):

	// `Value: @objc` is imaginary syntax for "any Objective-C-compatible type".
	var behavior objc<Value: @objc>: Value {
		accessor var _ objCName: StaticString = #name
		
		var value: Value
		get { return value }
		set { value = newValue }
		
		// The compiler knows about this behavior and looks in its `objCName` 
		// when it's writing the Objective-C metadata.
	}

But I'm digressing.)

With the basic accessor var feature in place—no fancy parameter syntax extensions needed—`initialValue` could be declared as an accessor var:

	var behavior lazy<Value>: Value {
		accessor var initialValue: Value
		
		private var value: Value?
		
		mutating get {
			value ??= initialValue()
			return value!
		}
	}
	
	@lazy var foo = Bar()
	// Exactly equivalent to:
	@lazy var foo: Bar {
		initialValue = Bar()
	}
	// Or:
	@lazy(initialValue=Bar()) var foo: Bar

If we take this course, then when the time comes to do names, we could of course use another magic `accessor var`:

	var behavior json<Value: JSONRepresentable where Self: JSONObject>: Value {
		accessor var name: String
		accessor var key: String = name
		…

That seems like it might be a feature worth having.

***

On the `@runcible` syntax, I believe I recommended using it pretty early on, so obviously I approve. :^) The proposal currently still shows the `[runcible]` syntax; I take it that just hasn't been updated? 

(I previously had a big section here talking about how to adapt behavior composition to this new syntax, but it grew into a horrendous monster inside an already long email, so I've cut it out to avoid cluttering the review. I'll send it to the list as a new thread.)

***

Incidentally, while I was writing up the JSON example, I thought of another item for the Future Directions list: behavior extensions. It'd be nice if I could do something like this:

	var behavior json<Value where Self: JSONObject>: Value {
		accessor var key: String
		
		accessor func toJSON(value: Value) -> JSONValue
		accessor func fromJSON(json: JSONValue) -> Value

		get {
			return fromJSON(self.json[key])
		}
		set {
			self.jsonObject[key] = toJSON(newValue)
		}
	}

	extension var behavior json where Value: JSONRepresentable {
		accessor func toJSON(value: Value) -> JSONValue {
			return value.json
		}
		accessor func fromJSON(json: JSONValue) -> Value {
			return Value(json: json as! Value.JSONRepresentation)
		}
	}

Behavior extensions would probably only be able to add accessor defaults and *maybe* methods and computed properties. I don't think they could add a setter, stored properties, initializers, or new accessors.

-- 
Brent Royal-Gordon
Architechies



More information about the swift-evolution mailing list