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

Joe Groff jgroff at apple.com
Thu Feb 11 12:30:27 CST 2016


> On Feb 10, 2016, at 10:13 PM, Chris Lattner <clattner at apple.com> wrote:
> 
> On Feb 10, 2016, at 2:00 PM, Douglas Gregor via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>> Proposal link:
>> https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md <https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md>
>>  <https://github.com/apple/swift-evolution#what-goes-into-a-review-1>What goes into a review?
>> 
>> The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:
>> 
>> What is your evaluation of the proposal?
> I’m +1 on the feature.  I have some comments for discussion though, I’m sorry I didn’t get these to you before the formal proposal went out:
> 
> 
> 
> First, on the most obvious bikeshed, the surface level syntax proposed:
> 
> 	var [lazy] foo = 1738
> 	foo.[lazy].clear()
> 
> 
> Given the recent trend to use # for compiler synthesized / macroish / magic syntax, it is worth considering whether:
> 
> 	var #lazy foo = 1738
> 	foo.#lazy.clear()
> 
> might make sense, or even:
> 
> 	 #lazy var foo = 1738
> 
> I think that something like this is a bit better aesthetically, since the [] delimiters are heavily array/collection/subscript-centric (which is a good thing).  OTOH, this would be giving users access to the “#” namespace, which means that future conflicts would have to be resolved with backticks - e.g. if they wanted a “line” behavior, they’d have to use "var #`line` foo = 1738”. I guess the fact that we already have a solution to the imagined problem means that this isn’t a big concern in practice.

Doesn't seem like an obvious analogy to me, since there's potentially a runtime component to these. As you noted, it also introduces a naming collision problem until we have a proper general solution for what '#' means, and I don't think we want to just shovel stuff ad-hoc into that namespace without more of a plan. We already overload '[]' for other things like capture lists, which isn't necessarily an excuse to overload it further, but we aren't sacrificing some pure design principle either.

> 
> In any case, I think we should just pick a syntax, land the feature, and keep bikeshedding on it through the end of Swift 3 :-)

Agreed.

> 
> 
> What are the semantics of behaviors applied to var/let decls with non-trivial patterns?  For example:
> 
> 	var [lazy] (foo, bar) = qux()
> 
> ?  While we could devise semantics for this in some cases, I think it is reasonable to only allow behaviors on single-identifier patterns, and reject the other cases as invalid.  If you agree, please loop that into the proposal.

Yeah, it's impossible to do if the behavior binds the initial value expression. We could potentially support it for behaviors with DI-like initialization. I had mentioned this but accidentally cut it out while trying to simplify, thanks for the heads up.

> I understand the need to specifying whether the getter/setter and other random methods are mutating or not, but doesn’t this eliminate the difference between a var/let behavior?  Couldn’t we just say that a behavior with a nonmutating getter & setter are valid on a “let”, and that any other mutating members are unavailable on a let?  This seems like a theoretical win, though I’m not sure whether it would actually be a win in practice for any conceivable behaviors you’ve been thinking about.

I think we should subset 'let' behaviors out for the same reason we don't yet allow computed 'let' properties—right now 'let' guarantees immutability, and we don't have a way in the language to enforce that for user code. It would definitely be nice to be able to declare 'let [delayed]' and 'var [delayed]', though.

> 
> 
> I like the approach of having a behavior decl, but am not excited about the decl syntax itself:
> behavior var [lazy] _: Value = initialValue {
> 
> I understand that this is intended to provide a template-like construct, but I have a few problems with this:
> 
> 1) A number of the things here are actually totally invariant (e.g. the _, the colon) or only serve to provide a name (Value, initialValue) but cannot be expanded into general expressions, though they look like they are.
> 
> 2) This is totally unprecedented in Swift.  We have a very regular structure of “@attributes decl-modifiers introducer_keyword name” - we have no declarations that look like this.  For example, while operator decls could follow this structure, they don’t.
> 
> 3) If you agree that we don’t need to differentiate between var/let then “behavior var" is providing potentially misleading info.
> 
> IMO, the most regular decl structure would be:
> 
> @initial_value_required      // or something like it.
> property behavior lazy {      // could be “subscript behavior” at some point in the future?
>    var value: Self? = nil        // Self is the property type?  Seems weird if it is the enclosing type.
>    ...
> }
> 
> or perhaps:
> 
> @initial_value_required
> property behavior lazy<PropertyType> {  // specify the type of the property as a generic constraint?
>    var value: PropertyType? = nil   
>    ...
> }
> 
> though I agree that this would require us to take “behavior” as a keyword.  Do we know whether or not this would be a problem in practice?  If so, going with “behavior var” and “behavior let” is probably ok.
> 
> 
> Of course, @initial_value_required is also pretty gross.  The alternative is to follow the precedent of operator decls, and introduce random syntax in their body, such as:
> 
> property behavior lazy<PropertyType> {
>    initializer initValue
>    var value: PropertyType? = nil   
>>        value = initValue
>    ...
> }
> 

Yeah, this is more like what I had proposed in previous drafts. If we go this route, I would rather use special contextual declarators like this than '@initial_value_required'-like attributes.


> 
> 
> 
>   // Behaviors can declare storage that backs the property.
>   private var value: Value?
> 
> What is the full range of access control allowed here?  Is there any reason to allow anything other than “public” (which would mean that the entity is exposed at whatever the properties access control level is)?  If so, why allow specifying internal?  For sake of exposition in the proposal, it seems simplest to say:
> 
> var value: Value?  // private by default
> 
> or something.
> 
> 
> 
> 
>   // Behaviors can declare initialization logic for the storage.
>   // (Stored properties can also be initialized in-line.)
>   init() {
>     value = nil
>   }
> 
> If a behavior has an init() with no arguments, then are the semantics that it is *always* run?  What if there are both an init() and an init(value : Value)?

As proposed there is no 'init(value: Value)'; we can design that later. An 'init()' with no arguments would always run at initialization time, as if:

var [behavior] x: Int

instantiated storage like:

var `x.[behavior].storage`: BehaviorStorage<Int> = `[behavior].init()`

> 
>  // Inline initializers are also supported, so `var value: Value? = nil`
>   // would work equivalently.
> 
> This example is using a stored property of type Optional<Value> which has a default value of nil, does that count or is this a syntactic requirement?  To me, it seems most natural to follow the existing rules we have:
> 
> 1) If you write any init, then you get what you write.
> 2) If there are no init’s, and any stored property in a behavior is non-default initializable, then it is an error.
> 3) If there are no init’s, and all stored properties in a behavior are default initializable, then you get init() implicitly.

I thought that was self-evident. I can make it explicit.

> 
> 
> Typographical comment:
>     if let value = value {
>       return value
>     }
> 
> You have way too many “value”s floating around, for sake of clarity, how about:
> 
>     if let valuePresent = value {
>       return valuePresent
>     }
> 
> 
> In "Resettable properties”, you have this:
> 
>   // Reset the property to its original initialized value.
>   mutating func reset() {
>     value = initialValue
>   }
> 
> This raises the question of how “initialValue” works:  Is it evaluated once when the property is bound and the resultant value is stored somewhere (evaluating any side effects exactly once) or is it an auto-closure-like concept?  If it is autoclosure-like, what does it mean in terms of requiring “self." qualification & @noescape?

In the proposal, I say it behaves like a get-only property—it evaluates every time it's loaded.

> 
> In the context of your @NSCopying-replacement example, I would want something like this:
> 
> class Foo {
>   var [copying] myproperty : NSString
> 
>   init(a : NSString) {
>      myproperty = a
>   }
> }
> 
> to work, where the behavior requires an initial value, but that value is provided by a flow-sensitive initialization point by DI .  How does this happen?

Yeah. DI-like initialization is one of the future directions.
> 
> 
> "This imposes an initializer requirement on the behavior. Any property using the behavior must be declared with an initial value"
> 
> Ah, this gets to the @NSCopying replacement example.  This might be too limiting, but so long as there is a path forward to generalize this, it seems like a fine starting point.
> 
> [[Coming back to this after reading to the end]] Ok, I see this is in the future directions section.  I’m fine with punting this to a further revision of the proposal, but I think that it is worthwhile to provide a syntactic affordance to differentiate properties that “must have an initializer expression” and “must be initialized by an 'init(v: Value)’ member on the behavior before otherwise used”.  @NSCopying replacement seems like the later case.  I’m fine with pushing off the general case to a later proposal, but we should carve out space for it to fit in.
> 
> 
> 
> 
> 
> public behavior var [changeObserved] _: Value = initialValue {
> ...
>    
>     if oldValue != newValue {
> 
> 
> This makes me vaguely uncomfortable, given that “changeObserved” has undeclared type requirements that are only diagnosed when the behavior is instantiated.

That was an oversight. In reality you'd have to declare:

public behavior var [changeObserved] _: Value = initialValue
  where Value: Equatable {

> 
> 
> 
> In detailed design:
> property-behavior-decl ::=
>   attribute* decl-modifier*
>   'behavior' 'var' '[' identifier ']' // behavior name
>   (identifier | '_')                  // property name binding
> If you go with this declaration syntax, I’d suggest requiring _ for the name, since the name isn’t otherwise used for anything.  Ah, it looks like you say this later, maybe the grammar just needs to be updated?
> A _ placeholder is required in the name position. (A future extension of behaviors may allow the property name to be bound as a string literal here.)
> 
> 
> 
> 
> "Inside a behavior declaration, self is implicitly bound to the value that contains the property instantiated using this behavior. For a freestanding property at global or local scope, this will be the empty tuple (), and for a static or class property, this will be the metatype. Within the behavior declaration, the type of self is abstract and represented by the implicit generic type parameter Self."
> 
> This is somewhat strange to me.  Would it be reasonable to say that self is bound iff the behavior has a classbound requirement on Self?  Alternatively, perhaps you could somehow declare/name the declcontext type, and refer to it later by a name other than Self?
> 
>   mutating func update(x: Int) {
>     [foo].x = x // Disambiguate reference to behavior storage
>   }
> 
> It would be really nice to be able to keep “self” referring to the behavior, and require an explicit declaration for the enclosing declaration if a behavior requires one (e.g. in the case of atomic).

Sure, we could take that approach. I don't think it makes sense to constrain access to the enclosing value to classes, though.

> "Nested Types in Behaviors”
> 
> This sounds great in the fullness of time, but it seems subsettable out of the first implementation/proposal.  Lacking this, it can be easily worked around with a private peer type.
> 
> 
> 
> 
> 
>     // Parameter gets the name 'bas' from the accessor requirement
>     // by default, as with built-in accessors today.
> 
> 
> Not really important for the v1 proposal, but it seems like a natural direction to allow this to be controlled by “API names” in the accessor.  The default would be that there are no API names, but if a behavior specified one, e.g.:
> 
> 		willSet(newValue newValue : Value)  
> 
> then “newValue” would be the default name in the body of the accessor.  If no “API name” were specified, then no name would be default injected.
> 
> 
> Typographical comment w.r.t. the "// Reinvent computed properties” example, you refer to the behavior as both “foobar” and “computed” later, I think the later reference is supposed to be "var [foobar] bar: Int”?  Feel free to find more diversity in your metasyntactic names here :-)
> 
> 
> 
> 
> 
> "Accessor requirements cannot take visibility modifiers; they are always as visible as the behavior itself."
> 
> Maybe I’m misinterpreting this, but I see accessors differently.  I see them as “never being accessible to the code that declares a property using the behavior”.  If you want this, you can define a method that wraps them or something.  To a client of a behaviors, accessors are “implementation only”.
> 
> 
> 
> 
> Can property requirements in protocols have behaviors on them?
I think not.

-Joe
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160211/31fb4a73/attachment.html>


More information about the swift-evolution mailing list