/property-behavior-decl.md Secret
Created
January 13, 2016 22:03
Revisions
-
jckarter created this gist
Jan 13, 2016 .There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,1385 @@ # Property Behaviors * Proposal: [SE-NNNN](https://github.com/apple/swift-evolution/proposals/NNNN-name.md) * Author(s): [Joe Groff](https://github.com/jckarter) * Status: **Review** * Review manager: TBD ## Introduction There are property implementation patterns that come up repeatedly. Rather than hardcode a fixed set of patterns into the compiler, we should provide a general "property behavior" mechanism to allow these patterns to be defined as libraries. ## Motivation We've tried to accommodate several important patterns for properties with targeted language support, but this support has been narrow in scope and utility. For instance, Swift 1 and 2 provide `lazy` properties as a primitive language feature, since lazy initialization is common and is often necessary to avoid having properties be exposed as `Optional`. Without this language support, it takes a lot of boilerplate to get the same effect: ```swift class Foo { // lazy var foo = 1738 private var _foo: Int? var foo: Int { get { if let value = _foo { return value } let initialValue = 1738 _foo = initialValue return initialValue } set { _foo = newValue } } } ``` Building `lazy` into the language has several disadvantages. It makes the language and compiler more complex and less orthogonal. It's also inflexible; there are many variations on lazy initialization that make sense, but we wouldn't want to hardcode language support for all of them. For instance, some applications may want the lazy initialization to be synchronized, but `lazy` only provides single-threaded initialization. The standard implementation of `lazy` is also problematic for value types. A `lazy` getter must be `mutating`, which means it can't be accessed from an immutable value. Inline storage is also suboptimal for many memoization tasks, since the cache cannot be reused across copies of the value. A value-oriented memoized property implementation might look very different, using a class instance to store the cached value out-of-line in order to avoid mutation of the value itself. Lazy properties are also unable to surface any additional operations over a regular property, such as to reset a lazy property's storage to be recomputed again. There are important property patterns outside of lazy initialization. It often makes sense to have "delayed", once-assignable-then-immutable properties to support multi-phase initialization: ```swift class Foo { let immediatelyInitialized = "foo" var _initializedLater: String? // We want initializedLater to present like a non-optional 'let' to user code; // it can only be assigned once, and can't be accessed before being assigned. var initializedLater: String { get { return _initializedLater! } set { assert(_initializedLater == nil) _initializedLater = newValue } } } ``` Implicitly-unwrapped optionals allow this in a pinch, but give up a lot of safety compared to a non-optional 'let'. Using IUO for multi-phase initialization gives up both immutability and nil-safety. We also have other application-specific property features like `didSet`/`willSet` and array addressors that add language complexity for limited functionality. Beyond what we've baked into the language already, there's a seemingly endless set of common property behaviors, including resetting, synchronized access, and various kinds of proxying, all begging for language attention to eliminate their boilerplate. ## Proposed solution I suggest we allow for **property behaviors** to be implemented within the language. A `var` or `let` declaration can specify its **behaviors** in square brackets after the keyword: ```swift var [lazy] foo = 1738 ``` which implements the property `foo` in a way described by the **property behavior declaration** for `lazy`: ```swift var behavior lazy<Value>: Value { var value: Value? = nil deferred initializer: Value get { if let value = value { return value } let initialValue = initializer value = initialValue return initialValue } set { value = newValue } } ``` Property behaviors can control the storage, initialization, and access of affected properties, obviating the need for special language support for `lazy`, observers, addressors, and other special-case property features. Property behaviors can also provide additional operations on properties, such as `clear`-ing a lazy property, accessed with `property.behavior` syntax: ```swift extension lazy { mutating func clear() { value = nil } } foo.lazy.clear() ``` ## Examples Before describing the detailed design, I'll run through some examples of potential applications for behaviors. ### Lazy The current `lazy` property feature can be reimplemented as a property behavior. ```swift // Property behaviors are declared using the `var behavior` keyword cluster. public var behavior lazy<Value>: Value { // Behaviors can declare storage that backs the property. private var value: Value? // Behaviors can declare that properties using the behavior require // a `deferred initializer` expression. When deferred, the // initializer expression is assumed to be evaluated after // initialization of the containing value, which allows it to refer // to `self`. If declared, `initializer` is bound in accessors and // methods of the behavior. deferred initializer: Value // Behaviors can declare initialization logic for the storage. // (Stored properties can also be initialized in-line.) init() { value = nil } // Inline initializers should also be supported, so `var value: Value? = nil` // would work. // Behaviors can declare accessors that implement the property. get { if let value = value { return value } // value = initializer } set { value = newValue } // Behaviors can also declare methods to attach to the property. public mutating func clear() { value = nil } } ``` Properties declared with the `lazy` behavior are backed by the `Optional`-typed storage and accessors from the behavior: ```swift var [lazy] x = 1738 // Allocates an Int? behind the scenes, inited to nil print(x) // Invokes the `lazy` getter, initializing the property x = 679 // Invokes the `lazy` setter ``` Visible members of the behavior can also be accessed under `property.behavior`: ```swift x.lazy.clear() // Invokes `lazy`'s `clear` method ``` ### Memoization Variations of `lazy` can be implemented that are more appropriate for certain situations. For instance, here's a `memoized` behavior that stores the cached value indirectly, making it suitable for immutable value types: ```swift public var behavior memoized<Value>: Value { public class MemoizationBox<Value> { var value: Value? = nil public func getOrEvaluate(fn: () -> Value) -> Value { if let value = value { return value } // Perform the initialization in a thread-safe way. // Implementation of 'sync' not shown here. return sync { let initialValue = fn() value = initialValue return initialValue } } func clear() { value = nil } } let storage: MemoizationBox<Value> = MemoizationBox() deferred initializer: Value get { return storage.getOrEvaluate { initializer } } public func clear() { storage.clear() } } ``` which can then be used like this: ```swift struct Location { let street, city, postalCode: String let [memoized] address = "\(street)\n\(city) \(postalCode)" } ``` ### Delayed Initialization A property behavior can model "delayed" initialization behavior, where the DI rules for properties are enforced dynamically rather than at compile time. This can avoid the need for implicitly-unwrapped optionals in multi-phase initialization use cases. We can implement both a mutable variant, which allows for reassignment like a `var`: ```swift public var behavior delayedMutable<Value>: Value { private var value: Value? = nil get { guard let value = value else { fatalError("property accessed before being initialized") } return value } set { value = newValue } // Perform an explicit initialization, trapping if the // value is already initialized. public mutating func initialize(initialValue: Value) { if let _ = value { fatalError("property initialized twice") } value = initialValue } } ``` and an immutable variant, which only allows a single initialization like a `let`: ```swift public var behavior delayedImmutable<Value>: Value { private var value: Value? = nil get { guard let value = value else { fatalError("property accessed before being initialized") } return value } // Perform an explicit initialization, trapping if the // value is already initialized. public mutating func initialize(initialValue: Value) { if let _ = value { fatalError("property initialized twice") } value = initialValue } } ``` This enables multi-phase initialization, like this: ```swift class Foo { var [delayedImmutable] x: Int init() { // We don't know "x" yet, and we don't have to set it } func initializeX(x: Int) { self.x.delayedImmutable.initialize(x) // Will crash if 'self.x' is already initialized } func getX() -> Int { return x // Will crash if 'self.x' wasn't initialized } } ``` #### Resettable properties There's a common pattern in Cocoa where properties are used as optional customization points, but can be reset to nil to fall back to a non-public default value. In Swift, properties that follow this pattern currently must be imported as ImplicitlyUnwrappedOptional, even though the property can only be *set* to nil. If expressed as a behavior, the `reset` operation can be decoupled from the type, allowing the property to be exported as non-optional: ```swift public var behavior resettable<Value>: Value { // A behavior can declare an *eager* initializer, which is available // during initialization of the property, but which cannot refer to // `self`. eager initializer: Value var value: Value = initializer get { return value } set { value = newValue } // Reset the property to its original initialized value. mutating func reset() { value = initializer } } ``` For example: ``` var [resettable] foo: Int = 22 print(foo) // => 22 foo = 44 print(foo) // => 44 foo.resettable.reset() print(foo) // => 22 ``` ### Property Observers A property behavior can also replicate the built-in behavior of `didSet`/`willSet` observers, by declaring support for custom accessors: ```swift public var behavior observed<Value>: Value { // For composability, a behavior can declare a `base` property. The // base can be inherited from another behavior or an overridden // superclass property; if neither of those are provided, a fresh stored // property is generated in the containing scope. base var value: Value // A behavior can also declare accessors, the implementations of which // must be provided by property declarations using the behavior. // The behavior may provide a default implementation of // the accessors, in order to make them optional. // The willSet accessor, invoked before the property is updated. The // default does nothing. accessor willSet(newValue: Value) { } // The didSet accessor, invoked before the property is updated. The // default does nothing. accessor didSet(oldValue: Value) { } get { return value } set { willSet(newValue) let oldValue = value value = newValue didSet(oldValue) } } ``` Because the `observed` behavior declares a **base property**, it can also be composed on top of other behaviors: ```swift var [lazy, observed] observedLazy = expensiveExpression() { didSet { print("\(oldValue) => \(observedLazy)") } } ``` or applied in subclasses that override base class properties, like `didSet` /`willSet` today: ```swift class Foo { var foo: Int } class NoisyFoo: Foo { override var [observed] foo: Int { didSet { print("\(oldValue) => \(foo)") } } } ``` A common complaint with `didSet`/`willSet` is that the observers fire on *every* write, not only ones that cause a real change. A behavior that supports a `didChange` accessor, which only gets invoked if the property value really changed to a value not equal to the old value, can be implemented as a new behavior: ```swift public var behavior changeObserved<Value: Equatable>: Value { base var value: Value mutating accessor didChange(oldValue: Value) { } get { return value } set { let oldValue = value value = newValue if oldValue != newValue { didChange(oldValue) } } } ``` For example: ```swift var [changeObserved] x = 1 { didChange { print("\(oldValue) => \(x)") } } x = 1 // Prints nothing x = 2 // Prints 1 => 2 ``` ### Synchronized Property Access Objective-C supports `atomic` properties, which take a lock on `get` and `set` to synchronize accesses to a property. This is occasionally useful, and it can be brought to Swift as a behavior. The real implementation of `atomic` properties in ObjC uses a global bank of locks, but for illustrative purposes (and to demonstrate referring to `self`) I'll use a per-object lock instead: ```swift // A class that owns a mutex that can be used to synchronize access to its // properties. public protocol Synchronizable: class { func withLock<R>(@noescape body: () -> R) -> R } // Behaviors can refer to a property's containing type using // `Self` (including to impose constraints on it). public var behavior synchronized<Value where Self: Synchronizable>: Value { base var value: Value get { return self.withLock { return value } } set { self.withLock { value = newValue } } } ``` For example: ```swift // `Synchronizable` conformances not presented here class SharedState: Synchronizable { var [synchronized] attributes: [String: Any] = [:] } class LazySharedState: Synchronizable { var [lazy, synchronized] attributes: [String: Any] = loadStateFromDisk() } class State: NSObject { var attributes: [String: Any] } class SynchronizedState: State, Synchronizable { override var [synchronized] attributes: [String: Any] } ``` ### `NSCopying` Many Cocoa classes implement value-like objects that require explicit copying. Swift currently provides an `@NSCopying` attribute for properties to give them behavior like Objective-C's `@property(copy)`, invoking the `copy` method on new objects when the property is set. We can turn this into a behavior: ```swift public var behavior copying<Value: NSCopying>: Value { base var value: Value get { return value } set { // Copy the value on reassignment. value = newValue.copy() } } ``` This is a small sampling of the possibilities of behaviors. Let's look at the proposed design in detail: ## Detailed design ### Property behavior declarations A **property behavior declaration** is introduced by the `var behavior` contextual keyword cluster, which declares the (possibly generic) type of properties supported by the behavior. ```text property-behavior-decl ::= attribute* decl-modifier* 'var' 'behavior' identifier generic-param-list? ':' type '{' property-behavior-member-decl* '}' ``` Inside the behavior declaration, standard initializer, property, method, and nested type declarations are allowed, as are **core accessor** declarations —`get` and `set`. Additional contextual declarations are supported for **initializer requirement declarations**, **base property declarations**, and **accessor requirement declarations**: ```text property-behavior-member-decl ::= decl property-behavior-member-decl ::= core-accessor-decl // get, set property-behavior-member-decl ::= property-behavior-initializer-decl property-behavior-member-decl ::= property-behavior-base-property-decl property-behavior-member-decl ::= property-behavior-accessor-decl // accessor foo(...) ``` ### Bindings within Behavior Declarations Definitions within behaviors can refer to other members of the behavior by unqualified lookup, or if disambiguation is necessary, by qualified lookup on the behavior: ```swift var behavior foo: Int { var x: Int init() { x = 1738 } mutating func update(x: Int) { foo.x = x // Disambiguate reference to behavior storage } } ``` If the behavior includes an *initializer requirement declaration*, then `initializer` is bound as a computed get-only property that evaluates the property's initializer expression: ```swift var behavior foo<Value>: Value { deferred initializer: Value get { return initializer } } ``` If the behavior includes *accessor requirement declarations*, then the declared accessor names are bound as functions with labeled arguments: ```swift var behavior fakeComputed<Value>: Value { accessor get() -> Value mutating accessor set(newValue: Value) get { return get() } set { set(newValue: newValue) } } ``` Note that the behavior's own *core accessor* implementations are *not* referenceable this way. 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`. Constraints can be placed on `Self` in the generic signature of the behavior, to make protocol members available on `self`: ```swift protocol Fungible { typealias Fungus func funge() -> Fungus } var behavior runcible<Value where Self: Fungible, Self.Fungus == Value>: Value { get { return self.funge() } } ``` Lookup within `self` is *not* implicit within behaviors and must always be explicit, since unqualified lookup refers to the behavior's own members. `self` is immutable except in `mutating` methods, where it is considered an `inout` parameter unless the `Self` type has a class constraint. `self` cannot be accessed within inline initializers of the behavior's storage or in `init` declarations, since these may run during the container's own initialization phase. ### Nested Types in Behaviors Behavior declarations may nest type declarations as a namespacing mechanism. As with other type declarations, the nested type cannot reference members from its enclosing behavior. ### Properties and Methods in Behaviors Behaviors may include property and method declarations. Any storage produced by behavior properties is expanded into the containing scope of a property using the behavior. ```swift var behavior runcible: Int { var x: Int = 0 let y: String = "" } var [runcible] a: Int // expands to: var `a.runcible.x`: Int let `a.runcible.y`: String var a: Int { ... } ``` For public behaviors, this is inherently *fragile*, so adding or removing storage is a breaking change. Resilience can be achieved by using a resilient type as storage. The instantiated properties must also be of types that are visible to potential users of the behavior, meaning that public behaviors must use storage with types that are either public or internal-with-availability, similar to the restrictions on inlineable functions. The properties and methods of the behavior are accessible from properties using the behavior, if they have sufficient visibility. ```swift var behavior runcible: Int { private var x: Int = 0 var y: String = "" func foo() {} } // In a different file... var [runcible] a: Int _ = a.runcible.x // Error, runcible.x is private _ = a.runcible.y // OK a.runcible.foo() // OK ``` Method and computed property implementations have only immutable access to `self` and their storage by default, unless they are `mutating`. (As with computed properties, setters are `mutating` by default unless explicitly marked `nonmutating`). ### Base Property A behavior may declare no more than one **base property**, which can be used to compose and override with behaviors. A base property is declared using the contextual `base` modifier on a `var` or `let` declaration: ```text property-behavior-base-property-decl ::= attribute* (decl-modifier | 'base')* core-property-decl // core-property-decl refers to the existing property-decl syntax without // `attribute* decl-modifier*` prefix ``` The base property must have the same type as the behavior. When a behavior declares a base property, a property using the behavior by itself instantiates the base property as a stored property, passing through any initialization: ```swift var behavior hasBase: Int { base var value: Int } var [hasBase] x: Int = 0 // `x.hasBase.base` instantiated as stored property var [hasBase] y: Int // `y.hasBase.base` instantiated as stored property y = 0 // Late initialization of `y` ``` A property may use multiple behaviors, if all behaviors after the first use a base property. Each behavior sees the result of the previous behavior as its base. ```swift var behavior root: Int { ... } var [root, hasBase] z: Int // Instantiates: // var [root] `z.hasBase.value`: Int // as the base for: // var [hasBase] z: Int ``` If a subclass `override`-s a property, it may apply behaviors to the overridden property, which will be used as its `base`. ``` class Foo { var x: Int = 0 } class Bar: Foo { override var [hasBase] x: Int // uses `super.x` as `x.hasBase.value` } ``` Inside a behavior declaration, the base property declaration cannot have an inline initializer or be initialized in an `init` declaration, since the underlying base property may not be owned by the behavior instantiation. A base property declaration also may not appear with an initializer requirement declaration. ### `init` in Behaviors The storage of a behavior must be initialized, either by inline initialization, or by an `init` declaration within the initializer: ```swift var behavior inlineInitialized: Int { var x: Int = 0 // initialized inline get { return x } } var behavior initInitialized: Int { var x: Int get { return x } init() { x = 0 } } ``` Behaviors can contain at most one `init` declaration, which must take no parameters. This `init` declaration cannot take a visibility modifier; it is always as visible as the behavior itself. Neither inline initializers nor `init` declaration bodies may reference `self`, since they will be executed during the initializing of a property's containing value. For the same reason, an `init` also may not refer to the `base` property, if any, nor may it invoke any accessor requirements or deferred initializer requirements. ### Accessor Requirement Declarations An *accessor requirement declaration* specifies that a behavior requires any property declared to use the behavior to provide an accessor implementation. An accessor requirement declaration is introduced by the contextual `accessor` keyword: ```swift property-behavior-accessor-decl ::= attribute* decl-modifier* 'accessor' identifier function-signature function-body? ``` An accessor requirement declaration looks like, and serves a similar role to, a function requirement declaration in a protocol. A property using the behavior must supply an implementation for each of its accessor requirements. The accessor names (with labeled arguments) are bound as functions within the behavior declaration: ```swift // Reinvent computed properties var behavior computed<Value>: Value { accessor get() -> Value mutating accessor set(newValue: Value) get { return get() } set { set(newValue: newValue) } } var [computed] foo: Int { get { return 0 } set { // Parameter gets the name 'newValue' from the accessor requirement // by default, as with built-in accessors today. print(newValue) } } var [computed] bar: Int { get { return 0 } set(myNewValue) { // Parameter name can be overridden as well print(myNewValue) } } ``` Accessor requirements can be made optional by specifying a default implementation: ```swift // Reinvent property observers var behavior observed<Value>: Value { base var value: Value mutating accessor willSet(newValue: Value) { // do nothing by default } mutating accessor didSet(oldValue: Value) { // do nothing by default } get { return value } set { willSet(newValue: newValue) let oldValue = value value = newValue didSet(oldValue: oldValue) } } ``` Accessor requirements cannot take visibility modifiers; they are always as visible as the behavior itself. Like methods, accessors are not allowed to mutate the storage of the behavior or `self` unless declared `mutating`. Mutating accessors can only be invoked by the behavior from other `mutating` contexts. ### Initializer Requirement Declarations A behavior can require an inline initializer expression with an *initializer requirement declaration*, specified by the contextual `initializer` keyword: ```text property-behavior-initializer-decl ::= ('eager' | 'deferred') 'initializer' ':' type ``` The type of `initializer` must match the type of the behavior. The initializer requirement must be declared as `eager` or `deferred`, indicating where in the initialization process the initializer expression is used: - An **eager initializer** is used during the initialization of the behavior's state. An eager initializer can be used in the `init` implementation of a behavior, or in the inline initializer of one or more of its instantiated stored properties. A property using the behavior cannot refer to `self` within its initializer expression, as in a normal stored property. ```swift var behavior eagerInit: Int { eager initializer: Int var value: Int init() { value = initializer // Allowed for eager initializer } get { return value + initializer } } class Foo { var [eagerInit] foo: Int = self.bar // Not allowed var bar: Int } ``` - A **deferred initializer** is used only after the initialization of the behavior's state. A deferred initializer cannot be referenced until the behavior's storage is initialized. A property using the behavior can refer to `self` within its initializer expression, as one would expect a `lazy` property to be able to. ```swift var behavior deferredInit: Int { eager initializer: Int var value: Int init() { value = initializer // Not allowed } get { return value + initializer } } class Foo { var [deferredInit] foo: Int = self.bar // OK var bar: Int } ``` When a behavior declares an initializer requirement, the name `initializer` is bound within the behavior declaration as a computed get-only property that evaluates the property's initializer expression. A property behavior with a `base` property member cannot require an initializer. A property behavior with an initializer requirement also cannot be applied to a property declaration with a destructuring pattern. ```swift var [deferredInit] (a, b) = tuple // Error ``` Initializer requirements do not take visibility modifiers; they are always as visible as the behavior. ### Core Accessor Declarations The behavior implements the property by defining its *core accessors*, `get` and optionally `set`. If a behavior only provides a getter, it produces read-only properties; if it provides both a getter and setter, it produces mutable properties (though properties that instantiate the behavior may still control the visibility of their setters). It is an error if a behavior declaration does not provide at least a getter. ### Using Behaviors in Property Declarations Property declarations gain the ability to declare behaviors, with arbitrary accessors: ```text property-decl ::= attribute* decl-modifier* core-property-decl core-property-decl ::= ('var' | 'let') behaviors? pattern-binding ((',' pattern-binding)+ | accessors)? behaviors ::= '[' visibility? decl-ref (',' visibility? decl-ref)* ']' pattern-binding ::= var-pattern (':' type)? inline-initializer? inline-initializer ::= '=' expr accessors ::= '{' accessor+ '}' | brace-stmt // see notes about disambiguation accessor ::= decl-modifier* decl-ref accessor-args? brace-stmt accessor-args ::= '(' identifier (',' identifier)* ')' ``` For example: ```swift public var [behavior1, public behavior2] prop: Int { accessor1 { body() } behavior1.accessor2(arg) { body() } behavior2.accessor3 { body() } } ``` If multiple properties are declared in the same declaration, the behaviors apply to every declared property. `let` properties cannot use behaviors in this proposal. When a property is declared with behaviors, the behaviors are instantiated left-to-right, with each instantiated property used as the base property of the next behavior instantiation. It is an error if any of the behaviors after the first don't declare a base property, or if the behavior cannot be instantiated for the property's type or `self` type. If the behaviors require accessors, the implementations for those accessors are taken from the property's accessor declarations, matching by name. If two behaviors require accessors of the same name, the accessor definitions can disambiguate using qualified names. If an accessor requirement takes parameters, but the definition in for the property does not explicitly name parameters, the parameter labels from the behavior's accessor requirement declaration are implicitly bound by default. ```swift var behavior foo: Int { accessor bar(arg: Int) get { return 0 } } var [foo] x: Int { bar { print(arg) } // `arg` implicitly bound } var [foo] x: Int { bar(myArg) { print(myArg) } // `arg` explicitly bound to `myArg` } ``` If any accessor definition in the property does not match up to a behavior requirement, it is an error. To preserve the shorthand for get-only computed properties, if the accessor declaration consists of code like a function body, that code is used as the implementation of a single accessor named `get`: ```swift var [foo] x: Int { return 0 } // is sugar for: var [foo] x: Int { get { return 0 } } ``` The parser resolves the ambiguity between implicit getter and named accessor syntax by reading ahead for `identifier ('.' identifier)* ('(' .* ')') '{'`, favoring parsing as a named accessor declaration if the lookahead succeeds. If a property with behaviors declares an inline initializer, it behaves as follows: - If the first behavior has an initializer requirement, the initializer expression is bound as the initializer when instantiating the behavior. An initializer requirement in a behavior can only be satisfied if the declaration is not destructuring. - If the first behavior declares a base property, and the base property is not satisfied by `override`-ing a super property, then the initializer expression is used to initialize the storage of the default stored base property. - Otherwise, it is an error to provide an inline initializer. ``` var behavior noInitializer: Int { get { return 0 } } var behavior initializerReqt: Int { deferred initializer: Int get { return 0 } } var behavior baseProp: Int { base var value: Int get { return 0 } } var [baseProp] x = 0 // OK, base property initialized to 0 var [initializerReqt] y = 0 // OK, satisfies initializer requirement var [initializerReqt] (a, b) = tuple // Error, behavior can't destructure var [noInitializer] z = 0 // Error, behavior doesn't support initializer ``` If a behavior instantiates a default stored base property, it may be an error to leave out the initializer, if the property is not initialized before use. ### Accessing Behavior Members on Properties A behavior's properties and methods can be accessed on properties using the behavior under `property.behavior`: ```swift var behavior foo: Int { var storage: Int = 0 func method() { } get { return storage } } var [foo] x: Int print(x.foo.storage) x.foo.method() ``` To access a behavior member, code must have visibility of both the property's behavior, and the behavior's member. Behaviors are `private` by default, unless declared with a higher visibility. A behavior cannot be more visible than the property it applies to. ```swift // foo.swift var behavior foo: Int { private var storage: Int = 0 func method() { } get { return storage } } // bar.swift var [foo] bar: Int var [internal foo] internalFoo: Int var [public foo] publicFoo: Int // Error, behavior more visible than property _ = bar.foo.storage // Error, `storage` is private to behavior bar.foo.method() // OK // bas.swift bar.foo.method() // Error, `foo` behavior is private internalFoo.foo.method() // OK ``` Methods, properties, and nested types within the behavior can be accessed. It is not allowed to access a behavior's `init` declaration, initializer or accessor requirements, or core accessors from outside the behavior declaration. ### Extensions on Behaviors Property behaviors should support extensions, to allow new methods or computed properties to be added, or to provide categorization: ```swift var behavior foo: Int { base var value: Int get { return value } } extension foo { mutating func inc() { value += 1 } } ``` As with struct or enum extensions, behavior extensions cannot introduce additional storage. Since behaviors are not types, behavior extensions cannot declare protocol conformances. Behavior extensions can however constrain the generic parameters of the behavior, including `Self`, to make the members available only on a subset of property and container types. ## Impact on existing code By itself, this is (almost, see below) an additive feature that doesn't impact existing code. However, it potentially obsoletes `lazy`, `willSet`/`didSet`, and `@NSCopying` as hardcoded language features. We could grandfather these in, but my preference would be to phase them out by migrating them to library-based property behavior implementations. (Removing them should be its own separate proposal, though.) One potentially breaking change is the disambiguation rule for implicit getter accessors. The rule favors parsing as a named accessor, which would break code that intends to define a getter whose first statement uses trailing closure syntax: ```swift var foo: Int { doSomething { // parsed as named `doSomething` accessor } return 0 } ``` The potential impact is amplified if qualified accessor names with explicit arguments are considered: ```swift var foo: Int { x.doSomething(arg) { // still parsed as named `x.doSomething` accessor } return 0 } ``` which strikes me as problematic. Ideas for a better disambiguation rule are welcome. An alternative to consider would be requiring non-core accessor implementations to be decorated with an `accessor` keyword, like the requirement declarations are in the behavior: It's also worth exploring whether property behaviors could replace the "addressor" mechanism used by the standard library to implement `Array` efficiently. It'd be great if the language only needed to expose the core conservative access pattern (`get`/`set`/`materializeForSet`) and let all variations be implemented as library features. Note that superseding `didSet`/`willSet` and addressors completely would require being able to apply behaviors to subscripts in addition to properties, which seems like a reasonable generalization. ## Alternatives considered ### Using a protocol (formal or not) instead of a new declaration A previous iteration of this proposal used an informal instantiation protocol for property behaviors, desugaring a behavior into function calls, so that: ```swift var [lazy] foo = 1738 ``` would act as sugar for something like this: ```swift var `foo.lazy` = lazy(var: Int.self, initializer: { 1738 }) var foo: Int { get { return `foo.lazy`[varIn: self, initializer: { 1738 }] } set { `foo.lazy`[varIn: self, initializer: { 1738 }] = newValue } } ``` There are a few disadvantages to this approach: - Behaviors would pollute the namespace, potentially with multiple global functions and/or types. - 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. - The property behavior logic ends up less clear, being encoded in unspecialized language constructs. - Determining the capabilities of a behavior relied on function overload resolution, which can be fiddly, and would require a lot of special case diagnostic work to get good, property-oriented error messages out of. - Without severely complicating the informal protocol, it would be difficult to support eager vs. deferred initializers, or allowing mutating access to `self` concurrently with the property's own storage without violating `inout` aliasing rules. The code generation for standalone behavior decls can hide this complexity. Making property behaviors a distinct declaration undeniably increases the language size, but the demand for something like behaviors is clearly there. In return for a new declaration, we get better namespacing, more efficient code generation, clearer, more descriptive code for their implementation, and more expressive power with better diagnostics. I argue that the complexity can pay for itself, today by eliminating several special-case language features, and potentially in the future by generalizing to other kinds of behaviors (or being subsumed by an all-encompassing macro system). For instance, a future `func behavior` could conceivably provide Python decorator-like behavior for transforming function bodies. ### Declaration syntax Alternatives to the proposed `var [behavior] propertyName` syntax include: - A different set of brackets, `var (behavior) propertyName` or `var {behavior} propertyName`. Parens have the problem of being ambiguous with a tuple `var` declaration, requiring lookahead to resolve. Square brackets also work better with other declarations behaviors could be extended to apply to in the future, such as subscripts or functions - An attribute, such as `@behavior(lazy)` or `behavior(lazy) var`. This is the most conservative answer, but is clunky. - Use the behavior function name directly as an attribute, so that e.g. `@lazy` works. - Use a new keyword, as in `var x: T by behavior`. - Something on the right side of the colon, such as `var x: lazy(T)`. To me this reads like `lazy(T)` is a type of some kind, which it really isn't. - Something following the property name, such as `var x«lazy»: T` or `var x¶lazy: T` (picking your favorite ASCII characters to replace `«»¶`). One nice thing about this approach is that it suggests `self.x«lazy»` as a declaration-follows-use way of accessing the backing property. ### Syntax for accessing the backing property The proposal suggests `x.behaviorName` for accessing the underlying backing property of `var (behaviorName) x`. The main disadvantage of this is that it complicates name lookup, which must be aware of the behavior in order to resolve the name, and is potentially ambiguous, since the behavior name could of course also be the name of a member of the property's type. Some alternatives to consider: - Reserving a keyword and syntactic form to refer to the backing property, such as `foo.x.behavior` or `foo.behavior(x)`. The problems with this are that reserving a keyword is undesirable, and that `behavior` is a vague term that requires more context for a reader to understand what's going on. If we support multiple behaviors on a property, it also doesn't provide a mechanism to distinguish between behaviors. - Something following the property name, such a `foo.x«lazy»` or `foo.x¶lazy` (choosing your favorite ASCII substitution for `«»¶`). - Doing member lookup in both the property's type and its behaviors (favoring the declared property when there are conflicts). If `foo` is known to be `lazy`, it's attractive for `foo.clear()` to Just Work without additional syntax. This has the usual ambiguity problems of overloading, of course; if the behavior's members are shadowed by the fronting type, something would be necessary to disambiguate. ### Core accessors for property behavior implementation The proposal as written specifies `get` and `set` as the only accessors that can be used to implement a property behavior. For efficiency, we may want to also allow the standard library to use addressors to implement standard behaviors, though it would be nice to design a fundamental core accessor model that subsumes addressors too. It'd also be a reasonable extension to allow a property behavior declaration to itself instantiate behaviors, and define itself in terms of those behaviors' accessors. ### Behaviors for immutable `let` properties A previous revision of this proposal allowed for behaviors to apply to `let` properties. Since we don't have an effects system (yet?), `let` behavior implementations have the potential to invalidate the immutability assumptions expected of `let` properties, and it would be the programmer's responsibility to maintain them. We don't support computed `let`s for the same reason, so I suggest leaving `let`s out of property behaviors for now. `let behavior`s could be added in the future when we have a comprehensive design for immutable computed properties and/or functions. ## TODO **Resilience rules for behaviors**: IMO, storage for behaviors should be fragile, since otherwise they require the equivalent of type metadata to abstract their layout across resilience boundaries. Other optimizations are possible depending on how much flexibility we want to allow in property behavior evolution. Some questions to answer include: - Can a behavior add new accessor requirements (with defaults)? - Can a behavior introduce references to `self` if it doesn't already use it? - Can a behavior freely change its private, non-stored-property members? - Can a behavior relax its generic constraints, on either the property type or on `Self`? Some fundamental constraints: - A behavior can't resiliently add, take away, or change the eagerness of an initializer requirement. - A behavior can't change whether it has a base property. - A behavior can't remove accessor requirements.