[Proposal] Property behaviors


(Joe Groff) #1

Hi everyone. Chris stole my thunder already—yeah, I've been working on a design for allowing properties to be extended with user-defined delegates^W behaviors. Here's a draft proposal that I'd like to open up for broader discussion. Thanks for taking a look!

-Joe

https://gist.github.com/jckarter/f3d392cf183c6b2b2ac3

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 property 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:

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 lazyonly provides single-threaded initialization. The standard implementation of lazy is also problematic for value types. A lazygetter 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:

class MemoizationBox<T> {
  var value: T? = nil
  init() {}
  func getOrEvaluate(fn: () -> T) -> T {
    if let value = value { return value }
    // Perform initialization in a thread-safe way.
    // Implementation of `sync` not shown here
    return sync {
      let initialValue = fn()
      value = initialValue
      return initialValue
    }
  }
}

struct Person {
  let firstName: String
  let lastName: String

  let _cachedFullName = MemoizationBox<String>()

  var fullName: String {
    return _cachedFullName.getOrEvaluate { "\(firstName) \(lastName)" }
  }
}
Lazy properties are also unable to surface any additional operations over a regular property. It would be useful to be able to reset a lazy property’s storage to be recomputed again, for instance, but this isn’t possible with lazy.

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:

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 behavior in parens after the keyword:

var (lazy) foo = 1738
which acts as sugar for something like this:

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
  }
}
Furthermore, the behavior can provide additional operations, such as clear-ing a lazy property, by accessing it with property.behavior syntax:

foo.lazy.clear()
(The syntax for declaring and accessing the behavior is up for grabs; I’m offering these only as a starting point.)

Property behaviors obviate the need for special language support for lazy, observers, addressors, and other special-case property behavior, letting us move their functionality into libraries and support new behaviors as well.

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:

public struct Lazy<Value> {
  var value: Value?

  public init() {
    value = nil
  }

  public subscript<Container>(varIn _: Container,
                              initializer initial: () -> Value) -> Value {
    mutating get {
      if let existingValue = value {
        return existingValue
      }
      let initialValue = initial()
      value = initialValue
      return initialValue
    }
    set {
      value = newValue
    }
  }
}

public func lazy<Value>(var type: Value.Type, initializer _: () -> Value)
    -> Lazy<Value> {
  return Lazy()
}
As mentioned above, lazy in Swift 2 doesn’t provide a way to reset a lazy value to reclaim memory and let it be recomputed later. A behavior can provide additional operations on properties that use the behavior; for instance, to clear a lazy property:

extension Lazy {
  public mutating func clear() {
    value = nil
  }
}

var (lazy) x = somethingThatEatsMemory()
use(x)
x.lazy.clear()
Memoization

Variations of lazy can be implemented that are more appropriate for certain situations. For instance, here’s a memoizedbehavior that stores the cached value indirectly, making it suitable for immutable value types:

public class MemoizationBox<Value> {
  var value: Value? = nil
  init() {}
  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
  }

  public subscript<Container>(letIn _: Container,
                              initializer value: () -> Value) -> Value {
    return box.getOrEvaluate(value)
  }
}

public func memoized<Value>(let type: Value.Type, initializer: () -> Value)
    -> MemoizationBox<Value> {
  return MemoizationBox()
}
Which can then be used like this:

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 var and let properties are enforced dynamically rather than at compile time:

public func delayed<Value>(let type: Value.Type) -> Delayed<Value> {
  return Delayed()
}
public func delayed<Value>(var type: Value.Type) -> Delayed<Value> {
  return Delayed()
}

public struct Delayed<Value> {
  var value: Value? = nil

  /// DI rules for vars:
  /// - Must be assigned before being read
  public subscript<Container>(varIn container: Container) {
    get {
      if let value = value {
        return value
      }
      fatalError("delayed var used before being initialized")
    }
    set {
      value = newValue
    }
  }

  /// DI rules for lets:
  /// - Must be initialized once before being read
  /// - Cannot be reassigned
  public subscript<Container>(letIn container: Container) {
    get {
      if let value = value {
        return value
      }
      fatalError("delayed let used before being initialized")
    }
  }

  /// Behavior operation to initialize a delayed variable
  /// or constant.
  public mutating func initialize(value: Value) {
    if let value = value {
      fatalError("delayed property already initialized")
    }
    self.value = value
  }
}
which can be used like this:

class Foo {
  let (delayed) x: Int

  init() {
    // We don't know "x" yet, and we don't have to set it
  }

  func initializeX(x: Int) {
    self.x.delayed.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:

public func resettable<Value>(var type: Value.Type,
                      initializer fallback: () -> Value) -> Resettable<Value> {
  return Resettable(value: fallback())
}
public struct Resettable<Value> {
  var value: Value?

  public subscript<Container>(varIn container: Container,
                              initializer fallback: () -> Value) -> Value {
    get {
      if let value = value { return value }
      return fallback()
    }
    set {
      value = newValue
    }
  }

  public mutating func reset() {
    value = nil
  }
}

var (resettable) foo: Int = 22
print(foo) // => 22
foo = 44
print(foo) // => 44
foo.resettable.reset()
print(foo) // => 22
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:

// A class that owns a mutex that can be used to synchronize access to its
// properties.
//
// `NSObject` could theoretically be extended to implement this using the
// object's `@synchronized` lock.
public protocol Synchronizable: class {
  func withLock<R>(@noescape body: () -> R) -> R
}

public func synchronized<Value>(var _: Value.Type,
                                initializer initial: () -> Value)
    -> Synchronized<Value> {
  return Synchronized(value: initial())
}

public struct Synchronized<Value> {
  var value: Value

  public subscript<Container: Synchronizable>(varIn container: Container,
                                              initializer _: () -> Value)
      -> Value {
    get {
      return container.withLock {
        return value
      }
    }
    set {
      container.withLock {
        value = newValue
      }
    }
  }
}
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:

public func copying<Value: NSCopying>(var _: Value.Type,
                                      initializer initial: () -> Value)
    -> Copying<Value> {
  return Copying(value: initial().copy())
}

public struct Copying<Value> {
  var value: Value

  public subscript<Container>(varIn container: Container,
                              initializer _: () -> Value)
      -> Value {
    get {
      return value
    }
    set {
      value = newValue.copy()
    }
  }
}
Referencing Properties with Pointers

We provide some affordances for interfacing properties with pointers for C interop and performance reasons, such as withUnsafePointer and implicit argument conversions. These affordances come with a lot of caveats and limitations. A property behavior can be defined that implements properties with manually-allocated memory, guaranteeing that pointers to the property can be freely taken and used:

public func pointable<Value>(var _: Value.Type,
                             initializer initial: () -> Value)
    -> Pointable<Value> {
  return Pointable(value: initial())
}

public class Pointable<Value> {
  public let pointer: UnsafeMutablePointer<Value>

  init(value: Value) {
    pointer = .alloc(1)
    pointer.initialize(value)
  }

  deinit {
    pointer.destroy()
    pointer.dealloc(1)
  }

  public subscript<Container>(varIn _: Container,
                              initializer _: () -> Value)
      -> Value {
    get {
      return pointer.memory
    }
    set {
      pointer.memory = newValue
    }
  }
}

var (pointable) x = 22
var (pointable) y = 44

memcpy(x.pointable.pointer, y.pointable.pointer, sizeof(Int.self))
print(x) // => 44
(Manually allocating and deallocating a pointer in a class is obviously not ideal, but is shown as an example. A production-quality stdlib implementation could use compiler magic to ensure the property is stored in-line in an addressable way.)

Property Observers

A property behavior can also replicate the built-in behavior of didSet/willSet observers:

typealias ObservingAccessor = (oldValue: Value, newValue: Value) -> ()

public func observed<Value>(var _: Value.Type,
                            initializer initial: () -> Value,
                            didSet _: ObservingAccessor = {},
                            willSet _: ObservingAccessor = {})
    -> Observed<Value> {
  return Observed(value: initial())
}

public struct Observed<Value> {
  var value: Value

  public subscript<Container>(varIn _: Container,
                              initializer _: () -> Value,
                              didSet didSet: ObservingAccessor = {},
                              willSet willSet: ObservingAccessor = {})
      -> Value {
    get { return value }
    set {
      let oldValue = value
      willSet(oldValue, newValue)
      value = newValue
      didSet(oldValue, newValue)
    }
  }
}
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:

public func changeObserved<Value: Equatable>(var _: Value.Type,
                                             initializer initial: () -> Value,
                                             didChange _: ObservingAccessor = {})
    -> ChangeObserved<Value> {
  return ChangeObserved(value: initial())
}

public struct ChangeObserved<Value: Equatable> {
  var value: Value

  public subscript<Container>(varIn _: Container,
                              initializer _: () -> Value,
                              didChange didChange: ObservingAccessor = {}) {
    get { return value }
    set {
      if value == newValue { return }
      value = newValue
      didChange(oldValue, newValue)
    }
  }
}
This is a small sampling of the possibilities of behaviors. Let’s look at how they can be implemented:

Detailed design
A property declaration can declare a behavior after the var or let keyword in parens:

var (runcible) foo: Int
(Possible alternatives to var (behavior) are discussed later.) Inside the parens is a dotted declaration reference that must refer to a behavior function that accepts the property attributes (such as its name, type, initial value (if any), and accessor methods) as parameters. How attributes map to parameters is discussed below.

When a property declares a behavior, the compiler expands this into a backing property, which is initialized by invoking the behavior function with the property’s attributes as arguments. The backing property takes on whatever type is returned by the behavior function. The declared property forwards to the accessors of the backing property’s subscript(varIn:...) (or subscript(letIn:...)) member, with self as the first argument (or () for a free variable declaration). The subscript may also accept any or all of the property’s attributes as arguments. Approximately, the expansion looks like this:

var `foo.runcible` = runcible(var: Int.self)
var foo: Int {
  return `foo.runcible`[varIn: self]
}
with the fine print that the property directly receives the get, set, materializeForSet, etc. accessors from the behavior’s subscript declaration. By forwarding to a subscript instead of separate get and set methods, property behaviors preserve all of the mutable property optimizations we support now and in the future for free. The subscript also determines the mutability of the declared property.

The behavior function is resolved by building a call with the following keyword arguments, based on the property declaration:

The metatype of the declared property’s type is passed as an argument labeled var for a var, or labeled let for a let.
If the declared property provides an initial value, the initial value expression is passed as a () -> T closure to an argument labeled initializer.
If the property is declared with accessors, their bodies are passed by named parameters corresponding to their names. Accessor names can be arbitrary identifiers.
For example, a property with a behavior and initial value:

var (runcible) foo = 1738
gets its backing property initialized as follows:

var `foo.runcible` = runcible(var: Int.self, initializer: { 1738 })
A property that declares accessor methods:

var (runcible) foo: Int {
  bar { print("bar") }
  bas(x) { print("bas \(x)") }
}
passes those accessors on to its behavior function:

private func `foo.bar`() { print("bar") }
private func `foo.bas`(x: T) { print("bar") }

var `foo.runcible` = runcible(var: Int.self,
                              bar: self.`foo.bar`,
                              bas: self.`foo.bas`)
Contextual types from the selected behavior function can be used to infer types for the accessors’ parameters as well as their default names. For example, if the behavior function is declared as:

func runcible<T>(var type: T.Type, bar: (newValue: T) -> ())
  -> RuncibleProperty<T>
then a bar accessor using this behavior can implicitly receive newValue as a parameter:

var (runcible) x: Int {
  bar { print("\(newValue.dynamicType)") } // prints Int
}
Once the behavior function has been resolved, its return type is searched for a matching subscript member with labeled index arguments:

The self value that contains the property is passed to a labeled varIn argument for a var, or a letIn argument for a let. This may be the metatype for a static property, or () for a global or local property.
After these arguments, the subscript must take the same labeled initializer and/or accessor closure arguments as the behavior function.
It is an error if a matching subscript can’t be found on the type. By constraining what types are allowed to be passed to the varIn or letIn parameter of the subscript, a behavior can constrain what kinds of container it is allowed to appear in.

By passing the initializer and accessor bodies to both the behavior function and subscript, the backing property can avoid requiring storage for closures it doesn’t need immediately at initialization time. It would be unacceptable if every lazy property needed to store its initialization closure in-line, for instance. The tradeoff is that there is potentially redundant work done forming these closures at both initialization and access time, and many of the arguments are not needed by both. However, if the behavior function and subscript are both inlineable, the optimizer ought to be able to eliminate dead arguments and simplify closures. For most applications, the attribute closures ought to be able to be @noescape as well.

Some behaviors may have special operations associated with them; for instance, a lazy property may provide a way to clear itself to reclaim memory and allow the value to be recomputed later when needed. The underlying backing property may be accessed by referencing it as property.behavior.

var (lazy) x = somethingThatEatsMemory()

use(x)
x.lazy.clear() // free the memory
The backing property has internal visibility by default (or private if the declared property is private). If the backing property should have higher visibility, the visibility can be declared next to the behavior:

public var (public lazy) x = somethingThatEatsMemory()
However, the backing property cannot have higher visibility than the declared property.

The backing property is always a stored var property. It is the responsibility of a let property behavior’s implementation to provide the expected behavior of an immutable property over it. A well behaved let should produce an identical value every time it is loaded, or die trying, as in the case of an uninitialized delayed let. A let should be safe to read concurrently from multiple threads. (In the fullness of time, an effects system might be able to enforce this, with escape hatches for internally-impure things like memoization of course.)

Impact on existing code
By itself, this is 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.)

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/to consider
Declaration syntax

Alternatives to the proposed var (behavior) propertyName syntax include:

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. This injects functions into the attribute namespace, which is problematic (but maybe not as much if the function itself also has to be marked with a @behavior_function attribute too).
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 «»¶, again), to match the similar proposed declaration syntax above.
“Overloading” the property name to refer to both the declared property and its backing property, and doing member lookup in both (favoring the declared property when there are conflicts). If foo.x is known to be lazy, it’s attractive for foo.x.clear() to Just Work without annotation. This has the usual ambiguity problems of overloading, of course; if the behavior’s members are shadowed by the fronting type, something incovenient like (foo.x as Lazy).clear() would be necessary to disambiguate.
Defining behavior requirements using a protocol

It’s reasonable to ask why the behavior interface proposed here is ad-hoc rather than modeled as a formal protocol. It’s my feeling that a protocol would be too constraining:

Different behaviors need the flexibility to require different sets of property attributes. Some kinds of property support initializers; some kinds of property have special accessors; some kinds of property support many different configurations. Allowing overloading (and adding new functionality via extensions and overloading) is important expressivity.
Different behaviors place different constraints on what containers are allowed to contain properties using the behavior, meaning that subscript needs the freedom to impose different generic constraints on its varIn/ letIn parameter for different behaviors.
It’s true that there are type system features we could theoretically add to support these features in a protocol, but increasing the complexity of the type system has its own tradeoffs. I think it’s unlikely that behaviors would be useful in generics either.

A behavior declaration

Instead of relying entirely on an informal protocol, we could add a new declaration to the language to declare a behavior, something like this:

behavior lazy<T> {
  func lazy(...) -> Lazy { ... }
  struct Lazy { var value: T; ... }
}
Doing this has some potential advantages:

It provides clear namespacing for things that are intended to be behaviors.
If the functions and types that implement the behavior can be nested under the behavior declaration somehow, then they don’t need to pollute the global function/type namespace.
The behavior declaration can explicitly provide metadata about the behavior, such as what container and value types it supports, what kinds of accessors properties can provide to it, that are all discovered by overload resolution in this proposal. It’d also be a natural place to place extensions like how a behavior behaves with overriding, what behaviors it can or can’t compose with, etc.
Naming convention for behaviors

This proposal doesn’t discuss the naming convention that behaviors should follow. Should they be random adjectives like lazy? Should we try to follow an -ing or -able suffix convention? Does it matter, if behaviors have their own syntax namespace?

TODO
When do properties with behaviors get included in the memberwise initializer of structs or classes, if ever? Can properties with behaviors be initialized from init rather than with inline initializers?

Can behaviors be composed, e.g. (lazy, observed), or (lazy, atomic)? How? Composition necessarily has to have an ordering, and some orderings will be wrong; e.g. one of (lazy, atomic) or (atomic, lazy) will be broken.

To be able to fully supplant didSet/willSet (and addressors), we’d need to be able to give behaviors to subscripts as well. The special override behavior of didSet/willSet in subclasses needs to be accounted for as well.

It’s worth considering what the “primitive” interface for properties is; after all, theoretically even computed properties could be considered a behavior if you unstack enough turtles. One key thing to support that I don’t think our current special-case accessors handle is conditional physical access. For instance, a behavior might want to pass through to its physical property, unless some form of transactionality is enabled. As a strawman, if there were an inout accessor, which received the continuation of the property access as an (inout T) -> Void parameter, that might be expressed like this:

var _x = 0
var x: Int {
  inout(continuation) {
    // If we're not logging, short-circuit to a physical access of `x`.
    if !logging {
      continuation(&_x)
      return
    }
    // Otherwise, save the oldValue and log before and after
    let oldValue = x
    var newValue = x
    continuation(&newValue)
    print("--- changing _x from \(oldValue) to \(newValue)")
    _x = newValue
    print("--- changed! _x from \(oldValue) to \(newValue)")
  }
}
An implementation of inout as proposed like this could be unrolled into a materializeForSet implementation using a SIL state machine transform, similar to what one would do to implement yield or await, which would check that continuation always gets called exactly once on all paths and capture the control flow after the continuation call in the materializeForSet continuation.
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#aftertoc>
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#>
Property Behaviors
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#propertybehaviors>Introduction
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#introduction>
Motivation
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#motivation>
Proposed solution
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#proposedsolution>
Examples
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#examples>Lazy
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#lazy>
Memoization
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#memoization>
Delayed Initialization
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#delayedinitialization>
Resettable properties
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#resettableproperties>
Synchronized Property Access
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#synchronizedpropertyaccess>
NSCopying
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#nscopying>
Referencing Properties with Pointers
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#referencingpropertieswithpointers>
Property Observers
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#propertyobservers>
Detailed design
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#detaileddesign>
Impact on existing code
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#impactonexistingcode>
Alternatives considered/to consider
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#alternativesconsideredtoconsider>Declaration syntax
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#declarationsyntax>
Syntax for accessing the backing property
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#syntaxforaccessingthebackingproperty>
Defining behavior requirements using a protocol
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#definingbehaviorrequirementsusingaprotocol>
A behavior declaration
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#abehaviordeclaration>
Naming convention for behaviors
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#namingconventionforbehaviors>
TODO
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#todo>


Atomic keyword for properties
(David Owens II) #2

The functionality looks like it is something that is definitely required to reduce the boiler-plate that Swift currently requires. However, this seems like a specific instance of a more general problem. For example, I run into a similar problem with working with other constructs, such as enums, where I want to provide specific “behaviors” for them that is just a bunch of boiler-plate code.

It seems like this proposal could be a starting place to start flush out what a macro/preprocessor/boiler-plate-reducer would look like in Swift. As such, I would like to see a syntax that could be extended beyond properties. Maybe this system is limited in scope to only allow this generation in specific contexts, like this property behavior, especially to scope this proposal down.

The short of it: I like the idea and it seems expandable to future concerns if syntax like attributes are used. And like you mentioned, these could be generate errors when used in the wrong contexts, such as lacking @behavior_function, or its equivalent.

-David


(Matthew Johnson) #3

I’ve really been looking forward to this proposal and I like this idea in general. I may have comments about specific details after giving it more thought.

I have one significant concern that is worth mentioning immediately. I would consider it extremely unfortunate if a delayed property behavior was considered sufficient in support of multi-phase initialization. Using a property behavior for that purpose gives no guarantee that the value is actually initialized at some point during instance initialization. IMO this is a major flaw. A runtime error might occur when accessing a delayed property after initialization is supposed to be complete.

Delayed property behaviors may have appropriate use cases but IMO they are not an adequate substitute for something that provides stronger guarantees for the common case of multi-phase initialization.

I very strongly prefer to see direct language support for multi-phase initialization. The compiler could provide most of the initialization guarantees it does for regular let properties. It could enforce single assignment in the initializer body and could prevent the initializer body itself from reading the delayed property before assignment.

The only guarantee that may not be possible is method calls to self during the second phase of initialization, but prior to assignment of all delayed properties (whether directly or by passing self to another instance) are potentially dangerous if they caused a read to a delayed property. The potential for error is significantly narrower with direct language support. As this is a very common use case (possibly the most common use case for delayed properties) I strongly believe it warrants direct language support.

Matthew

···

On Dec 17, 2015, at 11:37 AM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

Hi everyone. Chris stole my thunder already—yeah, I've been working on a design for allowing properties to be extended with user-defined delegates^W behaviors. Here's a draft proposal that I'd like to open up for broader discussion. Thanks for taking a look!

-Joe

https://gist.github.com/jckarter/f3d392cf183c6b2b2ac3

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 property 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:

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 lazyonly provides single-threaded initialization. The standard implementation of lazy is also problematic for value types. A lazygetter 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:

class MemoizationBox<T> {
  var value: T? = nil
  init() {}
  func getOrEvaluate(fn: () -> T) -> T {
    if let value = value { return value }
    // Perform initialization in a thread-safe way.
    // Implementation of `sync` not shown here
    return sync {
      let initialValue = fn()
      value = initialValue
      return initialValue
    }
  }
}

struct Person {
  let firstName: String
  let lastName: String

  let _cachedFullName = MemoizationBox<String>()

  var fullName: String {
    return _cachedFullName.getOrEvaluate { "\(firstName) \(lastName)" }
  }
}
Lazy properties are also unable to surface any additional operations over a regular property. It would be useful to be able to reset a lazy property’s storage to be recomputed again, for instance, but this isn’t possible with lazy.

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:

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 behavior in parens after the keyword:

var (lazy) foo = 1738
which acts as sugar for something like this:

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
  }
}
Furthermore, the behavior can provide additional operations, such as clear-ing a lazy property, by accessing it with property.behavior syntax:

foo.lazy.clear()
(The syntax for declaring and accessing the behavior is up for grabs; I’m offering these only as a starting point.)

Property behaviors obviate the need for special language support for lazy, observers, addressors, and other special-case property behavior, letting us move their functionality into libraries and support new behaviors as well.

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:

public struct Lazy<Value> {
  var value: Value?

  public init() {
    value = nil
  }

  public subscript<Container>(varIn _: Container,
                              initializer initial: () -> Value) -> Value {
    mutating get {
      if let existingValue = value {
        return existingValue
      }
      let initialValue = initial()
      value = initialValue
      return initialValue
    }
    set {
      value = newValue
    }
  }
}

public func lazy<Value>(var type: Value.Type, initializer _: () -> Value)
    -> Lazy<Value> {
  return Lazy()
}
As mentioned above, lazy in Swift 2 doesn’t provide a way to reset a lazy value to reclaim memory and let it be recomputed later. A behavior can provide additional operations on properties that use the behavior; for instance, to clear a lazy property:

extension Lazy {
  public mutating func clear() {
    value = nil
  }
}

var (lazy) x = somethingThatEatsMemory()
use(x)
x.lazy.clear()
Memoization

Variations of lazy can be implemented that are more appropriate for certain situations. For instance, here’s a memoizedbehavior that stores the cached value indirectly, making it suitable for immutable value types:

public class MemoizationBox<Value> {
  var value: Value? = nil
  init() {}
  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
  }

  public subscript<Container>(letIn _: Container,
                              initializer value: () -> Value) -> Value {
    return box.getOrEvaluate(value)
  }
}

public func memoized<Value>(let type: Value.Type, initializer: () -> Value)
    -> MemoizationBox<Value> {
  return MemoizationBox()
}
Which can then be used like this:

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 var and let properties are enforced dynamically rather than at compile time:

public func delayed<Value>(let type: Value.Type) -> Delayed<Value> {
  return Delayed()
}
public func delayed<Value>(var type: Value.Type) -> Delayed<Value> {
  return Delayed()
}

public struct Delayed<Value> {
  var value: Value? = nil

  /// DI rules for vars:
  /// - Must be assigned before being read
  public subscript<Container>(varIn container: Container) {
    get {
      if let value = value {
        return value
      }
      fatalError("delayed var used before being initialized")
    }
    set {
      value = newValue
    }
  }

  /// DI rules for lets:
  /// - Must be initialized once before being read
  /// - Cannot be reassigned
  public subscript<Container>(letIn container: Container) {
    get {
      if let value = value {
        return value
      }
      fatalError("delayed let used before being initialized")
    }
  }

  /// Behavior operation to initialize a delayed variable
  /// or constant.
  public mutating func initialize(value: Value) {
    if let value = value {
      fatalError("delayed property already initialized")
    }
    self.value = value
  }
}
which can be used like this:

class Foo {
  let (delayed) x: Int

  init() {
    // We don't know "x" yet, and we don't have to set it
  }

  func initializeX(x: Int) {
    self.x.delayed.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:

public func resettable<Value>(var type: Value.Type,
                      initializer fallback: () -> Value) -> Resettable<Value> {
  return Resettable(value: fallback())
}
public struct Resettable<Value> {
  var value: Value?

  public subscript<Container>(varIn container: Container,
                              initializer fallback: () -> Value) -> Value {
    get {
      if let value = value { return value }
      return fallback()
    }
    set {
      value = newValue
    }
  }

  public mutating func reset() {
    value = nil
  }
}

var (resettable) foo: Int = 22
print(foo) // => 22
foo = 44
print(foo) // => 44
foo.resettable.reset()
print(foo) // => 22
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:

// A class that owns a mutex that can be used to synchronize access to its
// properties.
//
// `NSObject` could theoretically be extended to implement this using the
// object's `@synchronized` lock.
public protocol Synchronizable: class {
  func withLock<R>(@noescape body: () -> R) -> R
}

public func synchronized<Value>(var _: Value.Type,
                                initializer initial: () -> Value)
    -> Synchronized<Value> {
  return Synchronized(value: initial())
}

public struct Synchronized<Value> {
  var value: Value

  public subscript<Container: Synchronizable>(varIn container: Container,
                                              initializer _: () -> Value)
      -> Value {
    get {
      return container.withLock {
        return value
      }
    }
    set {
      container.withLock {
        value = newValue
      }
    }
  }
}
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:

public func copying<Value: NSCopying>(var _: Value.Type,
                                      initializer initial: () -> Value)
    -> Copying<Value> {
  return Copying(value: initial().copy())
}

public struct Copying<Value> {
  var value: Value

  public subscript<Container>(varIn container: Container,
                              initializer _: () -> Value)
      -> Value {
    get {
      return value
    }
    set {
      value = newValue.copy()
    }
  }
}
Referencing Properties with Pointers

We provide some affordances for interfacing properties with pointers for C interop and performance reasons, such as withUnsafePointer and implicit argument conversions. These affordances come with a lot of caveats and limitations. A property behavior can be defined that implements properties with manually-allocated memory, guaranteeing that pointers to the property can be freely taken and used:

public func pointable<Value>(var _: Value.Type,
                             initializer initial: () -> Value)
    -> Pointable<Value> {
  return Pointable(value: initial())
}

public class Pointable<Value> {
  public let pointer: UnsafeMutablePointer<Value>

  init(value: Value) {
    pointer = .alloc(1)
    pointer.initialize(value)
  }

  deinit {
    pointer.destroy()
    pointer.dealloc(1)
  }

  public subscript<Container>(varIn _: Container,
                              initializer _: () -> Value)
      -> Value {
    get {
      return pointer.memory
    }
    set {
      pointer.memory = newValue
    }
  }
}

var (pointable) x = 22
var (pointable) y = 44

memcpy(x.pointable.pointer, y.pointable.pointer, sizeof(Int.self))
print(x) // => 44
(Manually allocating and deallocating a pointer in a class is obviously not ideal, but is shown as an example. A production-quality stdlib implementation could use compiler magic to ensure the property is stored in-line in an addressable way.)

Property Observers

A property behavior can also replicate the built-in behavior of didSet/willSet observers:

typealias ObservingAccessor = (oldValue: Value, newValue: Value) -> ()

public func observed<Value>(var _: Value.Type,
                            initializer initial: () -> Value,
                            didSet _: ObservingAccessor = {},
                            willSet _: ObservingAccessor = {})
    -> Observed<Value> {
  return Observed(value: initial())
}

public struct Observed<Value> {
  var value: Value

  public subscript<Container>(varIn _: Container,
                              initializer _: () -> Value,
                              didSet didSet: ObservingAccessor = {},
                              willSet willSet: ObservingAccessor = {})
      -> Value {
    get { return value }
    set {
      let oldValue = value
      willSet(oldValue, newValue)
      value = newValue
      didSet(oldValue, newValue)
    }
  }
}
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:

public func changeObserved<Value: Equatable>(var _: Value.Type,
                                             initializer initial: () -> Value,
                                             didChange _: ObservingAccessor = {})
    -> ChangeObserved<Value> {
  return ChangeObserved(value: initial())
}

public struct ChangeObserved<Value: Equatable> {
  var value: Value

  public subscript<Container>(varIn _: Container,
                              initializer _: () -> Value,
                              didChange didChange: ObservingAccessor = {}) {
    get { return value }
    set {
      if value == newValue { return }
      value = newValue
      didChange(oldValue, newValue)
    }
  }
}
This is a small sampling of the possibilities of behaviors. Let’s look at how they can be implemented:

Detailed design
A property declaration can declare a behavior after the var or let keyword in parens:

var (runcible) foo: Int
(Possible alternatives to var (behavior) are discussed later.) Inside the parens is a dotted declaration reference that must refer to a behavior function that accepts the property attributes (such as its name, type, initial value (if any), and accessor methods) as parameters. How attributes map to parameters is discussed below.

When a property declares a behavior, the compiler expands this into a backing property, which is initialized by invoking the behavior function with the property’s attributes as arguments. The backing property takes on whatever type is returned by the behavior function. The declared property forwards to the accessors of the backing property’s subscript(varIn:...) (or subscript(letIn:...)) member, with self as the first argument (or () for a free variable declaration). The subscript may also accept any or all of the property’s attributes as arguments. Approximately, the expansion looks like this:

var `foo.runcible` = runcible(var: Int.self)
var foo: Int {
  return `foo.runcible`[varIn: self]
}
with the fine print that the property directly receives the get, set, materializeForSet, etc. accessors from the behavior’s subscript declaration. By forwarding to a subscript instead of separate get and set methods, property behaviors preserve all of the mutable property optimizations we support now and in the future for free. The subscript also determines the mutability of the declared property.

The behavior function is resolved by building a call with the following keyword arguments, based on the property declaration:

The metatype of the declared property’s type is passed as an argument labeled var for a var, or labeled let for a let.
If the declared property provides an initial value, the initial value expression is passed as a () -> T closure to an argument labeled initializer.
If the property is declared with accessors, their bodies are passed by named parameters corresponding to their names. Accessor names can be arbitrary identifiers.
For example, a property with a behavior and initial value:

var (runcible) foo = 1738
gets its backing property initialized as follows:

var `foo.runcible` = runcible(var: Int.self, initializer: { 1738 })
A property that declares accessor methods:

var (runcible) foo: Int {
  bar { print("bar") }
  bas(x) { print("bas \(x)") }
}
passes those accessors on to its behavior function:

private func `foo.bar`() { print("bar") }
private func `foo.bas`(x: T) { print("bar") }

var `foo.runcible` = runcible(var: Int.self,
                              bar: self.`foo.bar`,
                              bas: self.`foo.bas`)
Contextual types from the selected behavior function can be used to infer types for the accessors’ parameters as well as their default names. For example, if the behavior function is declared as:

func runcible<T>(var type: T.Type, bar: (newValue: T) -> ())
  -> RuncibleProperty<T>
then a bar accessor using this behavior can implicitly receive newValue as a parameter:

var (runcible) x: Int {
  bar { print("\(newValue.dynamicType)") } // prints Int
}
Once the behavior function has been resolved, its return type is searched for a matching subscript member with labeled index arguments:

The self value that contains the property is passed to a labeled varIn argument for a var, or a letIn argument for a let. This may be the metatype for a static property, or () for a global or local property.
After these arguments, the subscript must take the same labeled initializer and/or accessor closure arguments as the behavior function.
It is an error if a matching subscript can’t be found on the type. By constraining what types are allowed to be passed to the varIn or letIn parameter of the subscript, a behavior can constrain what kinds of container it is allowed to appear in.

By passing the initializer and accessor bodies to both the behavior function and subscript, the backing property can avoid requiring storage for closures it doesn’t need immediately at initialization time. It would be unacceptable if every lazy property needed to store its initialization closure in-line, for instance. The tradeoff is that there is potentially redundant work done forming these closures at both initialization and access time, and many of the arguments are not needed by both. However, if the behavior function and subscript are both inlineable, the optimizer ought to be able to eliminate dead arguments and simplify closures. For most applications, the attribute closures ought to be able to be @noescape as well.

Some behaviors may have special operations associated with them; for instance, a lazy property may provide a way to clear itself to reclaim memory and allow the value to be recomputed later when needed. The underlying backing property may be accessed by referencing it as property.behavior.

var (lazy) x = somethingThatEatsMemory()

use(x)
x.lazy.clear() // free the memory
The backing property has internal visibility by default (or private if the declared property is private). If the backing property should have higher visibility, the visibility can be declared next to the behavior:

public var (public lazy) x = somethingThatEatsMemory()
However, the backing property cannot have higher visibility than the declared property.

The backing property is always a stored var property. It is the responsibility of a let property behavior’s implementation to provide the expected behavior of an immutable property over it. A well behaved let should produce an identical value every time it is loaded, or die trying, as in the case of an uninitialized delayed let. A let should be safe to read concurrently from multiple threads. (In the fullness of time, an effects system might be able to enforce this, with escape hatches for internally-impure things like memoization of course.)

Impact on existing code
By itself, this is 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.)

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/to consider
Declaration syntax

Alternatives to the proposed var (behavior) propertyName syntax include:

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. This injects functions into the attribute namespace, which is problematic (but maybe not as much if the function itself also has to be marked with a @behavior_function attribute too).
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 «»¶, again), to match the similar proposed declaration syntax above.
“Overloading” the property name to refer to both the declared property and its backing property, and doing member lookup in both (favoring the declared property when there are conflicts). If foo.x is known to be lazy, it’s attractive for foo.x.clear() to Just Work without annotation. This has the usual ambiguity problems of overloading, of course; if the behavior’s members are shadowed by the fronting type, something incovenient like (foo.x as Lazy).clear() would be necessary to disambiguate.
Defining behavior requirements using a protocol

It’s reasonable to ask why the behavior interface proposed here is ad-hoc rather than modeled as a formal protocol. It’s my feeling that a protocol would be too constraining:

Different behaviors need the flexibility to require different sets of property attributes. Some kinds of property support initializers; some kinds of property have special accessors; some kinds of property support many different configurations. Allowing overloading (and adding new functionality via extensions and overloading) is important expressivity.
Different behaviors place different constraints on what containers are allowed to contain properties using the behavior, meaning that subscript needs the freedom to impose different generic constraints on its varIn/ letIn parameter for different behaviors.
It’s true that there are type system features we could theoretically add to support these features in a protocol, but increasing the complexity of the type system has its own tradeoffs. I think it’s unlikely that behaviors would be useful in generics either.

A behavior declaration

Instead of relying entirely on an informal protocol, we could add a new declaration to the language to declare a behavior, something like this:

behavior lazy<T> {
  func lazy(...) -> Lazy { ... }
  struct Lazy { var value: T; ... }
}
Doing this has some potential advantages:

It provides clear namespacing for things that are intended to be behaviors.
If the functions and types that implement the behavior can be nested under the behavior declaration somehow, then they don’t need to pollute the global function/type namespace.
The behavior declaration can explicitly provide metadata about the behavior, such as what container and value types it supports, what kinds of accessors properties can provide to it, that are all discovered by overload resolution in this proposal. It’d also be a natural place to place extensions like how a behavior behaves with overriding, what behaviors it can or can’t compose with, etc.
Naming convention for behaviors

This proposal doesn’t discuss the naming convention that behaviors should follow. Should they be random adjectives like lazy? Should we try to follow an -ing or -able suffix convention? Does it matter, if behaviors have their own syntax namespace?

TODO
When do properties with behaviors get included in the memberwise initializer of structs or classes, if ever? Can properties with behaviors be initialized from init rather than with inline initializers?

Can behaviors be composed, e.g. (lazy, observed), or (lazy, atomic)? How? Composition necessarily has to have an ordering, and some orderings will be wrong; e.g. one of (lazy, atomic) or (atomic, lazy) will be broken.

To be able to fully supplant didSet/willSet (and addressors), we’d need to be able to give behaviors to subscripts as well. The special override behavior of didSet/willSet in subclasses needs to be accounted for as well.

It’s worth considering what the “primitive” interface for properties is; after all, theoretically even computed properties could be considered a behavior if you unstack enough turtles. One key thing to support that I don’t think our current special-case accessors handle is conditional physical access. For instance, a behavior might want to pass through to its physical property, unless some form of transactionality is enabled. As a strawman, if there were an inout accessor, which received the continuation of the property access as an (inout T) -> Void parameter, that might be expressed like this:

var _x = 0
var x: Int {
  inout(continuation) {
    // If we're not logging, short-circuit to a physical access of `x`.
    if !logging {
      continuation(&_x)
      return
    }
    // Otherwise, save the oldValue and log before and after
    let oldValue = x
    var newValue = x
    continuation(&newValue)
    print("--- changing _x from \(oldValue) to \(newValue)")
    _x = newValue
    print("--- changed! _x from \(oldValue) to \(newValue)")
  }
}
An implementation of inout as proposed like this could be unrolled into a materializeForSet implementation using a SIL state machine transform, similar to what one would do to implement yield or await, which would check that continuation always gets called exactly once on all paths and capture the control flow after the continuation call in the materializeForSet continuation.
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#aftertoc>
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#>
Property Behaviors
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#propertybehaviors>Introduction
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#introduction>
Motivation
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#motivation>
Proposed solution
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#proposedsolution>
Examples
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#examples>Lazy
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#lazy>
Memoization
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#memoization>
Delayed Initialization
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#delayedinitialization>
Resettable properties
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#resettableproperties>
Synchronized Property Access
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#synchronizedpropertyaccess>
NSCopying
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#nscopying>
Referencing Properties with Pointers
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#referencingpropertieswithpointers>
Property Observers
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#propertyobservers>
Detailed design
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#detaileddesign>
Impact on existing code
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#impactonexistingcode>
Alternatives considered/to consider
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#alternativesconsideredtoconsider>Declaration syntax
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#declarationsyntax>
Syntax for accessing the backing property
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#syntaxforaccessingthebackingproperty>
Defining behavior requirements using a protocol
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#definingbehaviorrequirementsusingaprotocol>
A behavior declaration
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#abehaviordeclaration>
Naming convention for behaviors
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#namingconventionforbehaviors>
TODO
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#todo>

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #4

I’ve really been looking forward to this proposal and I like this idea in general. I may have comments about specific details after giving it more thought.

I have one significant concern that is worth mentioning immediately. I would consider it extremely unfortunate if a delayed property behavior was considered sufficient in support of multi-phase initialization. Using a property behavior for that purpose gives no guarantee that the value is actually initialized at some point during instance initialization. IMO this is a major flaw. A runtime error might occur when accessing a delayed property after initialization is supposed to be complete.

Delayed property behaviors may have appropriate use cases but IMO they are not an adequate substitute for something that provides stronger guarantees for the common case of multi-phase initialization.

I very strongly prefer to see direct language support for multi-phase initialization. The compiler could provide most of the initialization guarantees it does for regular let properties. It could enforce single assignment in the initializer body and could prevent the initializer body itself from reading the delayed property before assignment.

The only guarantee that may not be possible is method calls to self during the second phase of initialization, but prior to assignment of all delayed properties (whether directly or by passing self to another instance) are potentially dangerous if they caused a read to a delayed property. The potential for error is significantly narrower with direct language support. As this is a very common use case (possibly the most common use case for delayed properties) I strongly believe it warrants direct language support.

Matthew


(Lily Ballard) #5

Hi everyone. Chris stole my thunder already—yeah, I've been working on
a design for allowing properties to be extended with user-defined
delegates^W behaviors. Here's a draft proposal that I'd like to open
up for broader discussion. Thanks for taking a look!

Thanks for posting this! I just read through it, and there's a lot to
like in here, but I also have a bunch of concerns. I'll go back through
the document in order and respond to bits of it.

I apologize in advance for the massive size of this email, and for its
rambling nature. It is a bit of stream-of-consciousness. I also
apologize if anything in here has already been addressed in this thread,
as I've been writing it over several hours and I know the thread has had
discussion during that time.

A var or let declaration can specify its behavior in parens after
the keyword

I like this syntax.

Furthermore, the behavior can provide additional operations, such
as clear-ing a lazy property, by accessing it with
property.behavior syntax:

You already mentioned this at the end, but I'm concerned about the
ambiguity between `foo.behavior` and `foo.someProp`. If the compiler
always resolves ambiguity in one way, that makes it impossible to
explicitly choose the alternative resolution (e.g. if `foo.lazy`
resolves in favor of a property of the type, how do you access the lazy
behavior? If it resolves in favor of the behavior, how do you get at the
property instead?). Not just that, but it's also ambiguous to any
reader; if I see `self.foo.bar` I have to know up-front whether "bar" is
a behavior or a property of the variable's type.

I'm mildly tempted to say we should use

`foo.lazy`.reset()

but I admit it does look a bit odd, especially if accessing methods of
behaviors ends up being common. Another idea might look like

foo.(lazy).reset()

Or maybe we could even come up with a syntax that lets you omit the
behavior name if it's unambiguous (e.g. only one behavior, or if the
method/property you're accessing only exists on one behavior). Being
able to omit the behavior name would be nice for defining resettable
properties because saying something like `foo.resettable.reset()` is
annoyingly redundant. Maybe something like `foo::reset()` or
`foo#reset()`, which would be shorthand for`foo::lazy.reset()` or
`foo#lazy.reset()`.

public subscript<Container>(varIn _: Container,
initializer initial: () -> Value) -> Value {

I'm a bit concerned about passing in the Container like this. For class
types it's probably fine, but for value types, it means we're passing a
copy of the value in to the property, which just seems really weird
(both because it's a copy, and because that copy includes a copy of the
property).

Also the only example you gave that actually uses the container is
Synchronized, but even there it's not great, because it means every
synchronized property in the class all share the same lock. But that's
not how Obj-C atomic properties work, and there's really no benefit at
all to locking the entire class when accessing a single property because
it doesn't provide any guarantees about access to multiple properties
(as the lock is unlocked in between each access).

FWIW, the way Obj-C atomic properties work is for scalars it uses atomic
unordered loads/stores (which is even weaker than memory_order_relaxed,
all it guarantees is that every load sees a value that was written at
some point, i.e. no half-written values). For scalars it calls functions
objc_copyStruct(), which uses a bank of 128 spinlocks and picks two of
them based on the hash of the src/dst addresses (there's a comment
saying the API was designed wrong, hence the need for 2 spinlocks;
ideally it would only use one lock based on the address of the property
because the other address is a local stack value). For objects it calls
objc_getProperty() / objc_setProperty() which uses a separate bank of
128 spinlocks (and picks one based on the address of the ivar). The
getter retains the object with the spinlock held and then autoreleases
it outside of the spinlock. The setter just uses the spinlock to protect
writing to the ivar, doing any retains/releases outside of it. I haven't
tested but it appears that Obj-C++ properties containing C++ objects
uses yet another bank of 128 spinlocks, using the spinlock around the
C++ copy operation.

Ultimately, the point here is that the only interesting synchronization
that can be done at the property level is unordered atomic access, and
for any properties that can't actually use an atomic load/store (either
because they're aggregates or because they're reference-counted objects)
you really do want to use a spinlock to minimize the cost. But adding a
spinlock to every single property is a lot of wasted space (especially
because safe spinlocks on iOS require a full word), which is why the Obj-
C runtime uses those banks of spinlocks.

In any case, I guess what I'm saying is we should ditch the Container
argument. It's basically only usable for classes, and even then it's
kind of strange for a property to actually care about its container.

var `foo.lazy` = lazy(var: Int.self, initializer: { 1738 })

This actually won't work to replace existing lazy properties. It's legal
today to write

lazy var x: Int = self.y + 1

This works because the initializer expression isn't actually run until
the property is accessed. But if the initializer is passed to the
behavior function, then it can't possibly reference `self` as that runs
before stage-1 initialization.

So we need some way to distinguish behaviors that initialize immediately
vs behaviors that initialize later. The former want an initializer on
the behavior function, and may or may not care about having an
initializer on the getter/setter. The latter don't want an initializer
on the behavior, and do want one on the getter/setter. In theory you
could use the presence of a declared `initializer` argument on the
behavior function to distinguish between eager-initialized and lazy-
initialized, though that feels a little odd.

let (memoized) address = "\(street)\n\(city) \(postalCode)"

You're using un-qualified accesses to properties on self in the
initializer here. I'm not actually against allowing that, but `lazy`
properties today require you to use `self.`, otherwise any unqualified
property access is resolved against the type instead of the value. I
believe the current behavior is because non-lazy properties resolve
unqualified properties this way, so `lazy` properties do too in order to
allow you to add `lazy` to any property without breaking the existing
initializer.

This property declaration also runs into the eager-vs-delayed
initializer issue I mentioned above.

A property behavior can model "delayed" initialization behavior, where
the DI rules for var and let properties are enforced dynamically
rather than at compile time

It looks to me that the only benefit this has versus IOUs is you can use
a `let` instead of a `var`. It's worth pointing out that this actually
doesn't even replace IOUs for @IBOutlets because it's commonly useful to
use optional-chaining on outlets for code that might run before the view
is loaded (and while optional chaining is possible with behavior access,
it's a lot more awkward).

let (delayed) x: Int ... self.x.delayed.initialize(x) ...

Allowing `let` here is actually a violation of Swift's otherwise-strict
rules about `let`. Specifically, Delayed here is a struct, but
initializing it requires it to be mutable. So `let (delayed) x: Int`
can't actually ever be initialized. You could make it a class, but
that's a fairly absurd performance penalty for something that provides
basically the same behavior as IOUs. You do remark later in detailed
design about how the backing storage is always `var`, which solves this
at a technical level, but it still appears to the user as though they're
mutating a `let` property and that's strictly illegal today.

I think the right resolution here is just to remove the `letIn`
constructor and use `var` for these properties. The behavior itself
(e.g. delayed) can document write-once behavior if it wants to. Heck,
that behavior was only enforcing write-once in a custom initialize()
method anyway, so nothing about the API would actually change.

Resettable properties

The implementation here is a bit weird. If the property is nil, it
invokes the initializer expression, every single time it's accessed. And
the underlying value is optional. This is really implemented basically
like a lazy property that doesn't automatically initialize itself.

Instead I'd expect a resettable property to have eager-initialization,
and to just eagerly re-initialize the property whenever it's reset. This
way the underlying storage isn't Optional, the initializer expression is
invoked at more predictable times, and it only invokes the initializer
once per reset.

The problem with this change is the initializer expression needs to be
provided to the behavior when reset() is invoked rather than when the
getter/setter is called.

NSCopying

We really need to support composition. Adding NSCopying to a property
doesn't really change the behavior of the property itself, it just makes
assignments to it automatically call copy() on the new value before
doing the actual assignment. Composition in general is good, but
NSCopying seems like an especially good example of where adding this
kind of behavior should work fine with everything else.

Based on the examples given here, there's really several different
things behaviors do:

* Behaviors that "decorate" the getter/setter, without actually changing
  the underlying value get/set. This includes property observers and
  Synchronized (although atomic properties ideally should alter the
  get/set to use atomic instructions when possible, but semantically
  it's the same as taking a per-property spinlock).
* Behaviors that transform the value. This is basically NSCopying,
  because it copies the value but otherwise wants to preserve any
  existing property behavior (just with the new value instead of the
  old). But e.g. lazy can also be thought of as doing this where the
  transform is from T to T? (the setter converts T into T? and assigns
  it to the underlying value; the getter unwraps the T? or initializes
  it if nil and returns T). Of course there is probably a difference
  between transformers that keep the same type and ones that change the
  type; e.g. property observers with NSCopying may want to invoke
  willSet with the initial uncopied value (in case the observer wants to
  change the assigned value), but didSet should of course be invoked
  with the resulting copied value. But transformers where the
  transformation is an implementation detail (such as lazy, which
  transforms T to T?) don't want to expose that implementation detail to
  the property observers. So maybe there's two types of transformers;
  one that changes the underlying type, and one that doesn't.
* Behaviors that don't alter the getter/setter but simply provide
  additional functionality. This is exemplified by Resettable (at least,
  with my suggested change to make it eagerly initialize), because it
  really just provides a .reset() function.
* The lazy vs eager initialized thing from before

I suspect that we really should have a behavior definition that
acknowledges these differences and makes them explicit in the API.

There's also a lot of composition concerns here. For example,
synchronized should probably always be the innermost decorator, because
the lock is really only protecting the storage of the value and
shouldn't e.g. cover property observers or NSCopying. Property observers
should probably always be the outermost decorator (and willSet should
even fire before NSCopying, partially because it should be whatever
value the user actually tried to assign, and because willSet observers
can actually change the value being assigned and any such new value
should then get copied by NSCopying).

Speaking of composition, mixing lazy and synchronized seems really
problematic. If Synchronized uses a bank of locks like the obj-c
runtime, then lazy can't execute inside of the lock because the
initializer might access something else that hits the same lock and
causes an unpredictable deadlock. But it can't execute outside of the
lock either because the initializer might then get executed twice
(which would surprise everyone). So really the combination of lazy +
synchronized needs to actually use completely separate combined
LazySynchronized type, one that provides the expected dispatch_once-
like behavior.

Referencing Properties with Pointers ... A production-quality stdlib
implementation could use compiler magic to ensure the property is
stored in-line in an addressable way.

Sounds like basically an implementation that just stores the value
inline as a value and uses Builtin.addressOf(). This behavior is
problematic for composition. It also doesn't work at all for computed
properties (although any behavior that directly controls value storage,
such as lazy, also has the same limitation). The behavior design should
acknowledge the split between behaviors that work on computed properties
and those that don't.

More thoughts on composition: The "obvious" way to compose behaviors is
to just have a chain of them where each behavior wraps the next one,
e.g. Copying<Resettable<Synchronized<NSString>>>. But this doesn't
actually work for properties like Lazy that change the type of the
underlying value, because the "underlying value" in this case is the
wrapped behavior, and you can't have a nil behavior (it would break most
of the functionality of behaviors, as well as break the ability to say
`foo.behavior.bar()`).

Based on the previous behavior categories, I'm tempted to say that we
need to model behaviors with a handful of protocols (e.g. on for
decorators, one for transformers, etc), and have the logic of the
property itself call the appropriate methods on the collection of
protocols at the appropriate times. Transformer behaviors could have an
associated type that is the transformed value type (and the behavior
itself would be generic, taking the value type as its parameter, as you
already have). The compiler can then calculate the ordering of
behaviors, and use the associated types to figure out the "real"
underlying value, and pass appropriately-transformed value types to the
various behaviors depending on where in the chain they execute. By that
I mean a chain of (observed, lazy, sync) for a property of type Int
(ignoring for a moment the issues with sync + lazy) would create an
Observed<Int>, a Lazy<Int>, and a Sync<Int?> (because the Lazy<Int>'s
associated type says it transforms to Int?). The problem with this model
is the behavior can no longer actually contain the underlying value as a
property. And that's actually fine. If we can split up any stored values
the behavior needs from the storage of the property itself, that's
probably a good thing.

Property Observers

Property Observers need to somehow support the behavior of letting
accessors reassign to the property without causing an infinite loop.
They also need to support subclassing such that the observers are called
in the correct order in the nested classes (and again, with
reassignment, such that the reassigned value is visible to the future
observers without starting the observer chain over again).

Property Observers also pose a special challenge for subclasses.
Overriding a property to add a behavior in many cases would actually
want to create brand new underlying storage (e.g. adding lazy to a
property needs different storage). But property observers explicitly
don't want to do that, they just want to observe the existing property.
I suspect this may actually line up quite well with the distinction
between decorators and other behaviors.

On a similar note, I'm not sure if there's any other behaviors where
overriding actually wants to preserve any existing behaviors. Property
observers definitely want to, but if I have a lazy property and I
override it in a subclass for any reason beyond adding observers, the
subclass property probably shouldn't be lazy. Conversely, if I have an
observed property and I override it to be lazy, it should still preserve
the property observers (but no other behaviors). This actually suggests
to me that Property Observers are unique among behaviors, and are
perhaps worthy of leaving as a language feature instead of as a
behavior. Of course, I can always override a property with a computed
property and call `super` in the getter/setter, at which point any
behaviors of the superclass property are expected to apply, but I don't
think there's any actual problems there.

Speaking of that, how do behaviors interact with computed properties? A
lazy computed property doesn't make sense (which is why the language
doesn't allow it). But an NSCopying computed property is fine (the
computed getter would be handed the copied value).

The backing property has internal visibility by default

In most cases I'd recommend private by default. Just because I have an
internal property doesn't mean the underlying implementation detail
should be internal. In 100% of the cases where I've written a computed
property backed by a second stored property (typically named with a _
prefix), the stored property is always private, because nobody has any
business looking at it except for the class/struct it belongs to.

Although actually, having said that, there's at least one behavior
(resettable) that only makes sense if it's just as visible as the
property itself (e.g. so it should be public on a public property).

And come to think of it, just because the class designer didn't
anticipate a desire to access the underlying storage of a lazy property
(e.g. to check if it's been initialized yet) doesn't mean the user of
the property doesn't have a reason to get at that.

So I'm actually now leaning to making it default to the same
accessibility as the property itself (e.g. public, if the property
is public). Any behaviors that have internal implementation details
that should never be exposed (e.g. memoized should never expose its
box, but maybe it should expose an accessor to check if it's
initialized) can mark those properties/methods as internal or
private and that accessibility modifier would be obeyed. Which is to
say, the behavior itself should always be accessible on a property,
but implementation details of the behavior are subject to the normal
accessibility rules there.

The proposed (public lazy) syntax can still be used to lower visibility,
e.g. (private lazy).

Defining behavior requirements using a protocol

As mentioned above, I think we should actually model behaviors using a
family of protocols. This will let us represent decorators vs value
transformers (and a behavior could even be both, by implementing both
protocols). We could also use protocols for eager initialization vs lazy
initialization (which is distinguished only by the presence of the
initializer closure in the behavior initializer). We'd need to do
something like

protocol Behavior { init(...) } protocol LazyBehavior { init(...) }
protocol DecoratorBehavior : Behavior { ... } protocol
LazyDecoratorBehavior : LazyBehavior { ... } protocol
TransformerBehavior : Behavior { ... } protocol LazyTransformerBehavior
: LazyBehavior { ... }

and that way a type could conform to both DecoratorBehavior and
TransformerBehavior without any collision in init (because the init
requirement comes from a shared base protocol).

As for actually defining the behavior name, you still do need the global
function, but it could maybe return the behavior type, e.g. behavior
functions are functions that match either of the following:

func name<T: Behavior>(...) -> T.Type func name<T:

(...) -> T.Type

I'm not really a big fan of having two "root" protocols here, but I also
don't like magical arguments (e.g. treating the presence of an argument
named "initializer" as meaningful) which is why the protocols take
initializers. I guess the protocols also need to declare typealiases for
the Value type (and TransformerBehavior can declare a separate typealias
for the TransformedValue, i.e. the underlying storage. e.g T? for lazy)

A behavior declaration

This has promise as well. By using a declaration like this, you can have
basically a DSL (using contextual keywords) to specify things like
whether it's lazy-initialized, decorators, and transformers. Same
benefits as the protocol family (e.g. good compiler checking of the
behavior definition before it's even used anywhere), allows for code code-
completion too, and it doesn't litter the global function namespace with
behavior names.

The more I think about this, the more I think it's a good idea.
Especially because it won't litter the global function namespace with
behavior names. Behavior constructors should not be callable by the
user, and behaviors may be named things we would love to use as function
names anyway (if a behavior implements some functionality that is useful
to be exposed to the user anyway, it can vend a type like your proposal
has and people can just instantiate that type directly).

Can properties with behaviors be initialized from init rather than
with inline initializers?

I think the answer to this has to be "absolutely". Especially if
property observers are a behavior (as the initial value may need to be
computed from init args or other properties, which can't be done as an
inline initializer).

-Kevin Ballard

···

On Thu, Dec 17, 2015, at 09:37 AM, Joe Groff via swift-evolution wrote:


(Michel Fortin) #6

About synchronized access, it seems to me that it'd be advantageous if implementing subscript in a behaviour was optional. This way you can force access of a property through a synchronized block which will help avoid races:

  var (synchronized) data: (x: Int, y: Int, z: Int)

  func test() {
    data.synchronize { (data) in
      data.x += 1
      data.y += data.x
      data.z += data.y
    }
  }

As for combining behaviours... Someone should have to do the work of combining them properly by creating a new behaviour type for the combined behaviour. To make this easier syntactically, we could have a behaviour combination function that would create a combined behaviour type. That function could be overloaded to support only behaviours that are compatible with each other.

Overall, this looks very good. But there is almost nothing said about derived classes and overriding. I very often override a property just so I can add a `didSet` clause, and if `didChange` was implemented I'd use it too. I'd very much like if those property behaviours could be compatible with overriding (where it makes sense). The way I see it, the behaviour should be able tell the compiler whether the overriding accessor should either replace or be prepended or appended to the overridden closure from the base class, or if overriding is simply not allowed.

···

Le 17 déc. 2015 à 12:37, Joe Groff via swift-evolution <swift-evolution@swift.org> a écrit :

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.

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(Félix Cloutier) #7

As I understand it, the current draft doesn't appear to support composability very well, though. If the backing property holds the storage, you won't be able, for instance, to have a `(observable, lazy)` property.

Should composability be a goal? Is there a way to classify behaviors that can and can't compose?

···

Le 17 déc. 2015 à 12:37:25, Joe Groff via swift-evolution <swift-evolution@swift.org> a écrit :

Hi everyone. Chris stole my thunder already—yeah, I've been working on a design for allowing properties to be extended with user-defined delegates^W behaviors. Here's a draft proposal that I'd like to open up for broader discussion. Thanks for taking a look!

-Joe

https://gist.github.com/jckarter/f3d392cf183c6b2b2ac3
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brent Royal-Gordon) #8

Hi everyone. Chris stole my thunder already—yeah, I've been working on a design for allowing properties to be extended with user-defined delegates^W behaviors. Here's a draft proposal that I'd like to open up for broader discussion. Thanks for taking a look!

This is fantastic, Joe.

The one big thing I would like to see from this feature is a way to get all the instances of a particular behavior which were attached to an instance. For example, I’d like to be able to annotate particular values with a `serialized` behavior and then have a serialization framework be able to access all the type’s `serialized` behavior instances and get or set the corresponding properties. Perhaps there’s a way to hack this in with what you’re doing already, but if so, I don’t see it yet.

Relatedly, it would be nice if a behavior got the name of the property and type it was being applied to. In addition to my serialization case, this would be useful for error messages and probably some other things.

It’s awesome and important that you can add arbitrary blocks like `didSet`, but I don’t see any mechanism to pass in any other sort of arbitrary parameter to a behavior. It’d be nice if you could do that.

Okay, time for bikeshedding:

I would really like to see this get the @ syntax. And specifically, I would like to see the @ syntax eventually *only* be used for behaviors and other user-extensible features like it, while anything that's pure, unalloyed compiler magic (like access modifiers) becomes a plain keyword. Obviously this would require some reorganization of existing features, and perhaps you wouldn’t do this all at once, but currently the @ syntax is not really used in any sort of principled way, and this seems like a good way to clean it up.

If you do adopt @ syntax, then the behavior instance could be accessed with propertyName@behaviorName.

I actually think that behavior instances should probably be private, and there should be no way to change that. Behaviors are inherently an implementation detail, and you should not be exposing them directly to users of your type. Instead, you should expose just the operations you expect to be needed through your own calls, which you can reimplement as needed. (This statement is not 100% compatible with my request for type-wide metadata, of course. Not sure how to reconcile those.)

Overall, great work! I can already tell that I’m going to use the shit out of this feature. Now to read Kevin Ballard’s monster reply…

···

--
Brent Royal-Gordon
Architechies


(plx) #9

I am excited by the general direction but I have some concerns about the scope of the design at this time; specifically, it seems like it would benefit a lot from having some flexible-and-efficient way for behaviors to “reach upward” back into their container from within their custom methods (without forcing the caller to pass-in the container to each such call, of course).

I took a stab at mocking up one of the behaviors I’d like to be able to write and hit a lot of roadblocks due to the above; I’ve included it below with some commentary.

Even though this is perhaps a rather extreme/niche behavior to want to implement, I think the issues it encountered are actually general enough that other useful behaviors will also encounter them under the proposal as sketched.

Here’s the sample use, starting with motivation.

For some APIs — e.g. CoreBluetooth — you often wind up with highly-stateful objects that receive callbacks on a specific queue, and typically also do their state-maintenance while on that same queue; these objects typically also have a “public API” with methods that are only meant for use while off the queue (e.g. from the main queue, to update the UI…).

You thus wind up with each method-and-property pretty clearly being one and only one of these:

- “on-queue”, e.g. *only* meant for use while on the object’s queue
- “off-queue”, e.g. *only* meant for use while off the object’s queue

…with concurrency-and-synchronization logic essentially amounting to only calling / using each method-and-property while appropriately on/off queue.

For a concrete example, for an implementer of CBCentralManagerDelegate:

- all the CBCentralManagerDelegate methods are "on-queue"
- all the BT-state-management methods (called in reaction to BT events) are also “on-queue”
- the public methods (e.g. for UI use, or for asking the object to do stuff) are “off-queue”
- some of the basic properties (status, is-peripheral-foo-connected?) are oddballs, and get:
  - private backing properties for use/update while on-queue
  - public off-queue accessors that do a dispatch_sync, read the backing property, and return it

…and so on.

This can all be handled today "by hand” — it just requires being careful — but it’d be nice to have a custom behavior that would streamline both the implementation of on/off queue access for properties, and make each site-of-use more self-documenting/self-verifying vis-a-vis on/off-queue status.

Here’s my best attempt (issues flagged in ALL-CAPS):

/// Object assumed to have private queue it uses for synchronization.
protocol PrivateQueueOwner : class {

  // we don’t want to leak the actual queue to the wider world,
  // so we have to bubble these up to the public API:
  func dispatchSync<R>(@noescape action: () -> R) -> R
  func dispatchBarrierSync<R>(@noescape action: () -> R) -> R
  func dispatchAsync(action: () -> ())
  func dispatchBarrierAsync(action: () -> ())

  // we assume we are managing our queues s.t. we can
  // actually get the below to work reliably:
  func isOnPrivateQueue() -> Bool

}

/// Behavior used to enforce a particular use-pattern around
/// a property of an object that uses a private queue for synchronization:
struct QueueAccess<Value> {
  var value: Value
  
  // THIS PART IS ONLY-KINDA OK:
  subscript<Container:PrivateQueueOwner>(varIn container: Container> {
    get {
      if container.isOnPrivateQueue() {
        return self.value
      } else {
        return self.container.dispatchSync() {
          return self.value
          // ^ DOES THIS ACTUALLY WORK AS I’D WANT, IF I AM A STRUCT?
        }
      }
    }
    set {
      if container.isOnPrivateQueue() {
        self.value = newValue
      } else {
        container.dispatchBarrierAsync() {
          self.value = newValue
          // ^ DOES THIS ACTUALLY WORK AS I’D WANT, IF I AM A STRUCT?
        }
      }
    }
  }

  // EVERYTHING FROM HERE ON DOWN IS MOSTLY PROBLEMATIC:

  func onQueueUpdate(newValue: Value) {
    assert(self.container.isOnPrivateQueue()) // <- HOW?
    self.value = newValue
  }
  
  func offQueueUpdate(newValue: Value) {
    assert(self.container.isOffPrivateQueue()) // <- HOW?
    self.container.dispatchBarrierAsync() { // <- HOW?
       self.value = newValue
       // ^ DOES THIS EVEN WORK IF I AM A STRUCT?
    }
  }

  func offQueueAccess() -> Value {
    assert(self.container.isOffPrivateQueue()) // <- HOW?
    return self.container.dispatchSync() { // <- HOW?
      return self.value
    }
  }

  func onQueueAcccess() -> Value {
    assert(self.container.isOnPrivateQueue()) // <- HOW?
    return self.value
  }

  func offQueueAccess<R>(@noescape transform: (Value) -> R) -> R {
    assert(self.container.isOffPrivateQueue()) // <- HOW?
    return self.container.dispatchSync() { // <- HOW?
      return transform(self.value)
    }
  }

  func onQueueAcccess<R>(@noescape transform: (Value) -> R) -> R {
    assert(self.container.isOnPrivateQueue()) // <- HOW?
    return transform(self.value)
  }

}

….which if it was implementable, would wind up used like so:

public class BTCentralManagerController : NSObject, CBCentralManagerDelegate, PrivateQueueOwner {

  internal lazy var centralManager: CBCentralManager = CBCentralManager(
    delegate: self,
    queue: self.privateQueue,
    options: self.centralManagerOptions()
  )
  
  private let privateQueue: dispatch_queue_t
  
  public private(set) var (queueAccess) centralManagerState: CBCentralManagerState = .Unknown
  internal private(set) var (queueAccess) peripheralControllers: [NSUUID:BTPeripheralController] = [:]
  
  // internal API sample:

  func centralManagerDidUpdateState(_ central: CBCentralManager) {
    self.centralManagerState.queueAccess.onQueueUpdate(central.state)
  }

  // public API sample

  public func peripheralControllerForUUID(uuid: NSUUID) -> BTPeripheralController? {
    // this is an explicitly “off-queue” method:
    self.currentState.queueAccess.offQueueAccess() {
      return $0[uuid]
    }
  }

}

…giving us:

- safe defaults for attempted direct-property access
- self-dcoumenting/self-validating customized getters-and-setters for all internal-use scenarios

But, as the # of ALL-CAPS comments should indicate, this behavior seems *well beyond* what the proposal can provide in a natural way (e.g. we can get closer by passing-in the container to each method, but that’s a lot less natural and a lot clunkier).

Moreover, even if the “ability to access the container” were to be addressed, I also don’t like having to use a protocol like `PrivateQueueOwner` to make my `queueAccess` behavior re-usable; at least at present, adopted-protocol visibility is the same visibility as the type itself, so that e.g. if a public class conforms to `PrivateQueueOwner` then all the methods in `PrivateQueueOwner` are also public.

This is undesirable, as although I’d want such classes to be public, I wouldn’t want such low-level implementation details to be part of their public API.

Ideally, rather than forcing the container to adopt a protocol, I could instead do something *more* like this:

public class BTCentralManagerController : NSObject, CBCentralManagerDelegate {

  private let privateQueue: dispatch_queue_t
  
  // configures `queueAccess` behavior to use `self.privateQueue` (with on-queue check also as-specified)
  // ...note that if this becomes possible, the syntax might need to change, b/c the below is not very readable!
  public private(set) var (queueAccess(queue: `self.privateQueue`, onQueue: `self.isOnPrivateQueue()`)) currentState: CBCentralManagerState = .Unknown
  internal private(set) var (queueAccess(queue: `self.privateQueue`, onQueue: `self.isOnPrivateQueue()`)) peripheralControllers: [NSUUID:BTPeripheralController] = [:]

}

// which somehow interacts with a declaration like this:
struct QueueAccess<Value> {

  var value: Value

  // declaration stating we expect a `queue` expression during configuration, with the
  // following type (and the accessor automagically-synthesized via compiler magic)
  container var queue: dispatch_queue_t { get }

  // declaration stating we expect an `onQueue` expression during configuration, with the
  // following type (and the implementation automagically-synthesized via compiler magic)
  container func onQueue() -> Bool

   func onQueueUpdate(newValue: Value) {
    assert(self.onQueue())
    self.value = newValue
  }
  
  func offQueueUpdate(newValue: Value) {
    assert(!self.onQueue())
    dispatch_barrier_async(self.queue) {
      self.value = newValue
      // ^ NOTE: this may still be a problem for struct-based behaviors...?
    }
  }

  // etc...

}

…which, if possible, would obviously make behaviors a lot more specialized than they are under the current proposal (e.g. they would seemingly need a lot of specialized help from the compiler to be able to access those variables without either creating a strong reference to the container or wasting a lot of space with redundant stored properties, and might be their own specialized type, rather than an otherwise-vanilla struct-or-class as per the current proposal).

But, if the above were possible, the behaviors would be a *lot* more re-usable, and it’d be unnecessary to have the container adopt a particular protocol.

Note also that even if the “unwanted API pollution” issue were resolved — e.g. by making it possible to somehow privately-adopt a protocol, or equivalent — there’d still be the issue of getting efficient access to the container to address, if these use cases are to be supported.

So that’s my reaction; if you read it this far, thanks for your attention.

I’d *completely* understand if the reaction here is simply that such uses are out-of-scope for this proposal; that seems perfectly reasonable!

But keep in mind, the general issue of reaching-up from custom methods of behaviors can show up in simpler contexts:

// convenience logic:
private extension UIViewController {

  func viewHasEnteredWindow() -> Bool {
     return self.viewIfLoaded()?.window != nil ?? false
  }
}

// custom behavior:
struct MustSetBeforeVisibility<Value> {
  value: Value?
  
  // THIS PART OK:
  subscript<Container:UIViewController>(varIn container: Container> -> Value? {
    get {
      if container.viewHasEnteredWindow() {
        guard let v = self.value else {
          fatalError(“Should’ve set property \(self) by now, b/c our VC’s view is in the window (vc: \(container))”)
        }
        return v
      } else {
        return self.value // perfectly-cromulent to be unset at this point in lifecycle
      }
    }
    set {
      if !container.viewHasEnteredWindow() {
        self.value = newValue
      } else {
        fatalError(“Not meant to be set after we have become potentially-visible!")
      }
    }
  }

  // HOW TO DO THIS PART:
  /// non-optional convenience accessor; only meant for use once our view has
  /// become potentially-visible
  func direct() -> Value {
    if !container.viewHasEnteredWindow() { // <- HOW?
       fatalError(“Trying to do direct-access on \(self) too soon!")
    } else if let v = self.value {
       return v
    } else {
       fatalError(“Trying to do direct-access, but never set the value for \(self) appropriately!")
    }
  }

}

…which is basically another take on a “smarter” implicitly-unwrapped-optional. You don’t *need* a function like `direct()`, but you might want it, and it might be nice to be able to differentiate “using too soon” and “forgot to set the value”.

···

On Dec 17, 2015, at 11:37 AM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

Hi everyone. Chris stole my thunder already—yeah, I've been working on a design for allowing properties to be extended with user-defined delegates^W behaviors. Here's a draft proposal that I'd like to open up for broader discussion. Thanks for taking a look!

-Joe

https://gist.github.com/jckarter/f3d392cf183c6b2b2ac3

Lazy
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#lazy>
Memoization
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#memoization>
Delayed Initialization
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#delayedinitialization>
Resettable properties
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#resettableproperties>
Synchronized Property Access
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#synchronizedpropertyaccess>
NSCopying
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#nscopying>
Referencing Properties with Pointers
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#referencingpropertieswithpointers>
Property Observers
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#propertyobservers>
Detailed design
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#detaileddesign>
Impact on existing code
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#impactonexistingcode>
Alternatives considered/to consider
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#alternativesconsideredtoconsider>Declaration syntax
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#declarationsyntax>
Syntax for accessing the backing property
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#syntaxforaccessingthebackingproperty>
Defining behavior requirements using a protocol
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#definingbehaviorrequirementsusingaprotocol>
A behavior declaration
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#abehaviordeclaration>
Naming convention for behaviors
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#namingconventionforbehaviors>
TODO
<file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#todo>

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Step C) #10

+1 for this proposal. I’m not clear on the syntax and need to take some
time exploring it though.

As I read the code in the proposal, it once again blurred the lines for me
between property and method. It often seems to me, at an intuitive /
teaching level, a property is just semantic sugar for a particular method,
combined with some secret sauce to manage the backing storage for the
variable.

Someone (Doug?) mentioned getting some documentation out that would clarify
the usage of property vs. method. I’d really like to see that for
comparison against my own evolving heuristics. It would also be useful in
discussing this proposal.

Hi everyone. Chris stole my thunder already—yeah, I've been working on a

design for allowing properties to be extended with user-defined
delegates^W behaviors. Here's a draft proposal that I'd like to open up
for broader discussion. Thanks for taking a look!

-Joe

https://gist.github.com/jckarter/f3d392cf183c6b2b2ac3

… snip …

Defining behavior requirements using a protocol

It’s reasonable to ask why the behavior interface proposed here is ad-hoc
rather than modeled as a formal protocol. It’s my feeling that a protocol
would be too constraining:

   - Different behaviors need the flexibility to require different sets
   of property attributes. Some kinds of property support initializers; some
   kinds of property have special accessors; some kinds of property support
   many different configurations. Allowing overloading (and adding new
   functionality via extensions and overloading) is important
   expressivity.
   - Different behaviors place different constraints on what containers
   are allowed to contain properties using the behavior, meaning that
   subscript needs the freedom to impose different generic constraints on
   its varIn/ letIn parameter for different behaviors.

Would a set of related protocols (given some suppositions above that we

could identify categories of behaviors that have similar needs) be another
option?

Instead of relying entirely on an informal protocol, we could add a new
declaration to the language to declare a behavior, something like this:

behavior lazy<T> {
  func lazy(...) -> Lazy { ... }
  struct Lazy { var value: T; ... }
}

I do like the idea of a behavior declaration. I find this to be relatively

easy to model mentally. It fits with Swift’s general use of the type system
to achieve both power and safety.

When do properties with behaviors get included in the memberwise
initializer of structs or classes, if ever? Can properties with behaviors
be initialized from init rather than with inline initializers?

There’s a separate discussion that mentioned allowing better control of
which initializers are generated or synthesized for a given struct. There’s
also been mention of a “derived” feature for adding conformance without
needing to supply a separate implementation. This question seems related to
me - it would be ideal if Swift had a coherent way to declare something
that did not need definition because it can be generated by the compiler.
In this case, to declare that a property is part of memberwise
initialization. `behavior lazy<T>: memberwise {` ?

Observable behaviors would not want to be precluded from initialization,
but would also not want to be fired on initialization - at least that’s my
first reaction. Is that true for all behaviors - that they would not want
to be fired if the property is set as part of the parent type’s
initialization, but only on later changes?


(Joe Groff) #11

I played around a bit with the idea of a special behavior declaration. I think it feels a lot nicer, though it also feels like a much bigger language change if we go this route. Inside the declaration, you need to specify:
- what accessors the behavior supports, to be implemented by properties using the behavior,
- if the behavior controls storage, what that storage is, and what initialization logic it requires,
- if the behavior requires an initializer, and whether that initializer is used eagerly at property initialization or deferred to later, and
- what operations the behavior offers, if any.

Here's a quick sketch of how a behavior declaration could look. As a strawman, I'll use 'var behavior' as the introducer for a property behavior (leaving the door open to 'func behavior', 'struct behavior', etc. in the possible future). If you were going to reinvent computed properties from whole cloth, that might look like this:

var behavior computed<T> {
  // A computed property requires a `get` and `set` accessor.
  accessor get() -> T
  accessor set(newValue: T)

  // Accessors for the property
  get { return get() }
  set { set(newValue) }
}

lazy might look something like this:

var behavior lazy<T> {
  // lazy requires an initializer expression, but it isn't
  // used until after object initialization.
  deferred initializer: T

  // The optional storage for the property.
  var value: T?

  // Initialize the storage to nil.
  init() {
    value = nil
  }

  // Accessors for the property.
  mutating get {
    if let value = value {
      return value
    }
    // `initializer` is implicitly bound to the initializer expr as a
    // `@noescape () -> T` within the behavior's members.
    let initialValue = initializer()
    value = initialValue
    return initialValue
  }

  set {
    value = newValue
  }

  // clear() operation for the behavior.
  mutating func clear() {
    value = nil
  }
}

Some behaviors like `lazy` and `resettable` want to take control of the storage to manage their semantics, but many behaviors are adapters independent of how the underlying behavior behaves. These kinds of behavior are easy to compose with other behaviors and to override base class properties with. You could use inheritance-like syntax to indicate a "wrapping" behavior like this, and commandeer `super` to refer to the underlying property. For instance, `synchronized`:

var behavior synchronized<T>: T {
  get {
    return sync { return super }
  }
  set {
    return sync { return super }
  }
}

or `observing` didSet/willSet:

var behavior observing<T>: T {
  accessor willSet(oldValue: T, newValue: T) { }
  accessor didSet(oldValue: T, newValue: T) { }

  get { return super }
  set {
    let oldValue = super
    willSet(oldValue, newValue)
    super = newValue
    didSet(oldValue, newValue)
  }
}

If you want to refer back to the containing `self`, we could support that too, and by treating behavior functions specially we should be able to maintain coherent semantics for backreferencing value types as well. Implementing `synchronized` with a per-object lock could look like this:

var behavior synchronizedByObject<T>: T where Self: Synchronizable {
  get {
    return self.withLock { return super }
  }
  set {
    return self.withLock { return super }
  }
}

(though the juxtaposed meanings of `super` and `self` here are weird together…we'd probably want a better implicit binding name for the underlying property.)

-Joe


(Tino) #12

Quite complex proposal… guess I'll read it a second time when I'm less tired, but I have already one idea for the feature:

Syntax for accessing the backing property

When I saw the examples, I wondered right away "what if foo has an own property called runcible? How is the type modified to allow access to its backing property?" (that was my first interpretation on "foo.runcible")
I think all problems can be avoided without new keywords or a suffix with rarely used (or forbidden) character:
Just give the backing property for "foo" the name "super.foo".
- Afaik this is safe, because you can't create a property that already exists in the superclass (I guess it works if the property is not visible…)
- It's quite intuitive to me, because super always bypasses the normal behavior of self (well, commonly it refers to the superclass, but "bypass self" is just more general than superclass).

May the force be with you
Tino


(Joe Groff) #13

Yes. I'm working on a revision I hope to post soon.

-Joe

···

On Jan 13, 2016, at 7:36 AM, Wallacy <wallacyf@gmail.com> wrote:

Just to clarify a little, this proposal is (will be) addressed to 3.0?

Em qui, 17 de dez de 2015 às 15:41, Joe Groff via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> escreveu:
Hi everyone. Chris stole my thunder already—yeah, I've been working on a design for allowing properties to be extended with user-defined delegates^W behaviors. Here's a draft proposal that I'd like to open up for broader discussion. Thanks for taking a look!

-Joe

https://gist.github.com/jckarter/f3d392cf183c6b2b2ac3

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 property 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:

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 lazyonly provides single-threaded initialization. The standard implementation of lazy is also problematic for value types. A lazygetter 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:

class MemoizationBox<T> {
  var value: T? = nil
  init() {}
  func getOrEvaluate(fn: () -> T) -> T {
    if let value = value { return value }
    // Perform initialization in a thread-safe way.
    // Implementation of `sync` not shown here
    return sync {
      let initialValue = fn()
      value = initialValue
      return initialValue
    }
  }
}

struct Person {
  let firstName: String
  let lastName: String

  let _cachedFullName = MemoizationBox<String>()

  var fullName: String {
    return _cachedFullName.getOrEvaluate { "\(firstName) \(lastName)" }
  }
}
Lazy properties are also unable to surface any additional operations over a regular property. It would be useful to be able to reset a lazy property’s storage to be recomputed again, for instance, but this isn’t possible with lazy.

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:

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 behavior in parens after the keyword:

var (lazy) foo = 1738
which acts as sugar for something like this:

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
  }
}
Furthermore, the behavior can provide additional operations, such as clear-ing a lazy property, by accessing it with property.behavior syntax:

foo.lazy.clear()
(The syntax for declaring and accessing the behavior is up for grabs; I’m offering these only as a starting point.)

Property behaviors obviate the need for special language support for lazy, observers, addressors, and other special-case property behavior, letting us move their functionality into libraries and support new behaviors as well.

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:

public struct Lazy<Value> {
  var value: Value?

  public init() {
    value = nil
  }

  public subscript<Container>(varIn _: Container,
                              initializer initial: () -> Value) -> Value {
    mutating get {
      if let existingValue = value {
        return existingValue
      }
      let initialValue = initial()
      value = initialValue
      return initialValue
    }
    set {
      value = newValue
    }
  }
}

public func lazy<Value>(var type: Value.Type, initializer _: () -> Value)
    -> Lazy<Value> {
  return Lazy()
}
As mentioned above, lazy in Swift 2 doesn’t provide a way to reset a lazy value to reclaim memory and let it be recomputed later. A behavior can provide additional operations on properties that use the behavior; for instance, to clear a lazy property:

extension Lazy {
  public mutating func clear() {
    value = nil
  }
}

var (lazy) x = somethingThatEatsMemory()
use(x)
x.lazy.clear()
Memoization

Variations of lazy can be implemented that are more appropriate for certain situations. For instance, here’s a memoizedbehavior that stores the cached value indirectly, making it suitable for immutable value types:

public class MemoizationBox<Value> {
  var value: Value? = nil
  init() {}
  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
  }

  public subscript<Container>(letIn _: Container,
                              initializer value: () -> Value) -> Value {
    return box.getOrEvaluate(value)
  }
}

public func memoized<Value>(let type: Value.Type, initializer: () -> Value)
    -> MemoizationBox<Value> {
  return MemoizationBox()
}
Which can then be used like this:

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 var and let properties are enforced dynamically rather than at compile time:

public func delayed<Value>(let type: Value.Type) -> Delayed<Value> {
  return Delayed()
}
public func delayed<Value>(var type: Value.Type) -> Delayed<Value> {
  return Delayed()
}

public struct Delayed<Value> {
  var value: Value? = nil

  /// DI rules for vars:
  /// - Must be assigned before being read
  public subscript<Container>(varIn container: Container) {
    get {
      if let value = value {
        return value
      }
      fatalError("delayed var used before being initialized")
    }
    set {
      value = newValue
    }
  }

  /// DI rules for lets:
  /// - Must be initialized once before being read
  /// - Cannot be reassigned
  public subscript<Container>(letIn container: Container) {
    get {
      if let value = value {
        return value
      }
      fatalError("delayed let used before being initialized")
    }
  }

  /// Behavior operation to initialize a delayed variable
  /// or constant.
  public mutating func initialize(value: Value) {
    if let value = value {
      fatalError("delayed property already initialized")
    }
    self.value = value
  }
}
which can be used like this:

class Foo {
  let (delayed) x: Int

  init() {
    // We don't know "x" yet, and we don't have to set it
  }

  func initializeX(x: Int) {
    self.x.delayed.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:

public func resettable<Value>(var type: Value.Type,
                      initializer fallback: () -> Value) -> Resettable<Value> {
  return Resettable(value: fallback())
}
public struct Resettable<Value> {
  var value: Value?

  public subscript<Container>(varIn container: Container,
                              initializer fallback: () -> Value) -> Value {
    get {
      if let value = value { return value }
      return fallback()
    }
    set {
      value = newValue
    }
  }

  public mutating func reset() {
    value = nil
  }
}

var (resettable) foo: Int = 22
print(foo) // => 22
foo = 44
print(foo) // => 44
foo.resettable.reset()
print(foo) // => 22
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:

// A class that owns a mutex that can be used to synchronize access to its
// properties.
//
// `NSObject` could theoretically be extended to implement this using the
// object's `@synchronized` lock.
public protocol Synchronizable: class {
  func withLock<R>(@noescape body: () -> R) -> R
}

public func synchronized<Value>(var _: Value.Type,
                                initializer initial: () -> Value)
    -> Synchronized<Value> {
  return Synchronized(value: initial())
}

public struct Synchronized<Value> {
  var value: Value

  public subscript<Container: Synchronizable>(varIn container: Container,
                                              initializer _: () -> Value)
      -> Value {
    get {
      return container.withLock {
        return value
      }
    }
    set {
      container.withLock {
        value = newValue
      }
    }
  }
}
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:

public func copying<Value: NSCopying>(var _: Value.Type,
                                      initializer initial: () -> Value)
    -> Copying<Value> {
  return Copying(value: initial().copy())
}

public struct Copying<Value> {
  var value: Value

  public subscript<Container>(varIn container: Container,
                              initializer _: () -> Value)
      -> Value {
    get {
      return value
    }
    set {
      value = newValue.copy()
    }
  }
}
Referencing Properties with Pointers

We provide some affordances for interfacing properties with pointers for C interop and performance reasons, such as withUnsafePointer and implicit argument conversions. These affordances come with a lot of caveats and limitations. A property behavior can be defined that implements properties with manually-allocated memory, guaranteeing that pointers to the property can be freely taken and used:

public func pointable<Value>(var _: Value.Type,
                             initializer initial: () -> Value)
    -> Pointable<Value> {
  return Pointable(value: initial())
}

public class Pointable<Value> {
  public let pointer: UnsafeMutablePointer<Value>

  init(value: Value) {
    pointer = .alloc(1)
    pointer.initialize(value)
  }

  deinit {
    pointer.destroy()
    pointer.dealloc(1)
  }

  public subscript<Container>(varIn _: Container,
                              initializer _: () -> Value)
      -> Value {
    get {
      return pointer.memory
    }
    set {
      pointer.memory = newValue
    }
  }
}

var (pointable) x = 22
var (pointable) y = 44

memcpy(x.pointable.pointer, y.pointable.pointer, sizeof(Int.self))
print(x) // => 44
(Manually allocating and deallocating a pointer in a class is obviously not ideal, but is shown as an example. A production-quality stdlib implementation could use compiler magic to ensure the property is stored in-line in an addressable way.)

Property Observers

A property behavior can also replicate the built-in behavior of didSet/willSet observers:

typealias ObservingAccessor = (oldValue: Value, newValue: Value) -> ()

public func observed<Value>(var _: Value.Type,
                            initializer initial: () -> Value,
                            didSet _: ObservingAccessor = {},
                            willSet _: ObservingAccessor = {})
    -> Observed<Value> {
  return Observed(value: initial())
}

public struct Observed<Value> {
  var value: Value

  public subscript<Container>(varIn _: Container,
                              initializer _: () -> Value,
                              didSet didSet: ObservingAccessor = {},
                              willSet willSet: ObservingAccessor = {})
      -> Value {
    get { return value }
    set {
      let oldValue = value
      willSet(oldValue, newValue)
      value = newValue
      didSet(oldValue, newValue)
    }
  }
}
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:

public func changeObserved<Value: Equatable>(var _: Value.Type,
                                             initializer initial: () -> Value,
                                             didChange _: ObservingAccessor = {})
    -> ChangeObserved<Value> {
  return ChangeObserved(value: initial())
}

public struct ChangeObserved<Value: Equatable> {
  var value: Value

  public subscript<Container>(varIn _: Container,
                              initializer _: () -> Value,
                              didChange didChange: ObservingAccessor = {}) {
    get { return value }
    set {
      if value == newValue { return }
      value = newValue
      didChange(oldValue, newValue)
    }
  }
}
This is a small sampling of the possibilities of behaviors. Let’s look at how they can be implemented:

Detailed design
A property declaration can declare a behavior after the var or let keyword in parens:

var (runcible) foo: Int
(Possible alternatives to var (behavior) are discussed later.) Inside the parens is a dotted declaration reference that must refer to a behavior function that accepts the property attributes (such as its name, type, initial value (if any), and accessor methods) as parameters. How attributes map to parameters is discussed below.

When a property declares a behavior, the compiler expands this into a backing property, which is initialized by invoking the behavior function with the property’s attributes as arguments. The backing property takes on whatever type is returned by the behavior function. The declared property forwards to the accessors of the backing property’s subscript(varIn:...) (or subscript(letIn:...)) member, with self as the first argument (or () for a free variable declaration). The subscript may also accept any or all of the property’s attributes as arguments. Approximately, the expansion looks like this:

var `foo.runcible` = runcible(var: Int.self)
var foo: Int {
  return `foo.runcible`[varIn: self]
}
with the fine print that the property directly receives the get, set, materializeForSet, etc. accessors from the behavior’s subscript declaration. By forwarding to a subscript instead of separate get and set methods, property behaviors preserve all of the mutable property optimizations we support now and in the future for free. The subscript also determines the mutability of the declared property.

The behavior function is resolved by building a call with the following keyword arguments, based on the property declaration:

The metatype of the declared property’s type is passed as an argument labeled var for a var, or labeled let for a let.
If the declared property provides an initial value, the initial value expression is passed as a () -> T closure to an argument labeled initializer.
If the property is declared with accessors, their bodies are passed by named parameters corresponding to their names. Accessor names can be arbitrary identifiers.
For example, a property with a behavior and initial value:

var (runcible) foo = 1738
gets its backing property initialized as follows:

var `foo.runcible` = runcible(var: Int.self, initializer: { 1738 })
A property that declares accessor methods:

var (runcible) foo: Int {
  bar { print("bar") }
  bas(x) { print("bas \(x)") }
}
passes those accessors on to its behavior function:

private func `foo.bar`() { print("bar") }
private func `foo.bas`(x: T) { print("bar") }

var `foo.runcible` = runcible(var: Int.self,
                              bar: self.`foo.bar`,
                              bas: self.`foo.bas`)
Contextual types from the selected behavior function can be used to infer types for the accessors’ parameters as well as their default names. For example, if the behavior function is declared as:

func runcible<T>(var type: T.Type, bar: (newValue: T) -> ())
  -> RuncibleProperty<T>
then a bar accessor using this behavior can implicitly receive newValue as a parameter:

var (runcible) x: Int {
  bar { print("\(newValue.dynamicType)") } // prints Int
}
Once the behavior function has been resolved, its return type is searched for a matching subscript member with labeled index arguments:

The self value that contains the property is passed to a labeled varIn argument for a var, or a letIn argument for a let. This may be the metatype for a static property, or () for a global or local property.
After these arguments, the subscript must take the same labeled initializer and/or accessor closure arguments as the behavior function.
It is an error if a matching subscript can’t be found on the type. By constraining what types are allowed to be passed to the varIn or letIn parameter of the subscript, a behavior can constrain what kinds of container it is allowed to appear in.

By passing the initializer and accessor bodies to both the behavior function and subscript, the backing property can avoid requiring storage for closures it doesn’t need immediately at initialization time. It would be unacceptable if every lazy property needed to store its initialization closure in-line, for instance. The tradeoff is that there is potentially redundant work done forming these closures at both initialization and access time, and many of the arguments are not needed by both. However, if the behavior function and subscript are both inlineable, the optimizer ought to be able to eliminate dead arguments and simplify closures. For most applications, the attribute closures ought to be able to be @noescape as well.

Some behaviors may have special operations associated with them; for instance, a lazy property may provide a way to clear itself to reclaim memory and allow the value to be recomputed later when needed. The underlying backing property may be accessed by referencing it as property.behavior.

var (lazy) x = somethingThatEatsMemory()

use(x)
x.lazy.clear() // free the memory
The backing property has internal visibility by default (or private if the declared property is private). If the backing property should have higher visibility, the visibility can be declared next to the behavior:

public var (public lazy) x = somethingThatEatsMemory()
However, the backing property cannot have higher visibility than the declared property.

The backing property is always a stored var property. It is the responsibility of a let property behavior’s implementation to provide the expected behavior of an immutable property over it. A well behaved let should produce an identical value every time it is loaded, or die trying, as in the case of an uninitialized delayed let. A let should be safe to read concurrently from multiple threads. (In the fullness of time, an effects system might be able to enforce this, with escape hatches for internally-impure things like memoization of course.)

Impact on existing code
By itself, this is 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.)

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/to consider
Declaration syntax

Alternatives to the proposed var (behavior) propertyName syntax include:

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. This injects functions into the attribute namespace, which is problematic (but maybe not as much if the function itself also has to be marked with a @behavior_function attribute too).
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 «»¶, again), to match the similar proposed declaration syntax above.
“Overloading” the property name to refer to both the declared property and its backing property, and doing member lookup in both (favoring the declared property when there are conflicts). If foo.x is known to be lazy, it’s attractive for foo.x.clear() to Just Work without annotation. This has the usual ambiguity problems of overloading, of course; if the behavior’s members are shadowed by the fronting type, something incovenient like (foo.x as Lazy).clear() would be necessary to disambiguate.
Defining behavior requirements using a protocol

It’s reasonable to ask why the behavior interface proposed here is ad-hoc rather than modeled as a formal protocol. It’s my feeling that a protocol would be too constraining:

Different behaviors need the flexibility to require different sets of property attributes. Some kinds of property support initializers; some kinds of property have special accessors; some kinds of property support many different configurations. Allowing overloading (and adding new functionality via extensions and overloading) is important expressivity.
Different behaviors place different constraints on what containers are allowed to contain properties using the behavior, meaning that subscript needs the freedom to impose different generic constraints on its


(Joe Groff) #14

Thanks everyone for the first round of feedback on my behaviors proposal. I've revised it with the following changes:

- Instead of relying on mapping behaviors to function or type member lookup, I've introduced a new purpose-built 'var behavior' declaration, which declares the accessor and initializer requirements and provides the storage and behavior methods of the property. I think this gives a clearer design for authoring behaviors, and allows for a more efficient and flexible implementation model.
- I've backed off from trying to include 'let' behaviors. As many of you noted, it's better to tackle immutable computed properties more holistically than to try to backdoor them in.
- I suggest changing the declaration syntax to use a behavior to square brackets—'var [behavior] foo'—which avoids ambiguity with destructuring 'var' bindings, and also works with future candidates for behavior decoration, particularly `subscript`.

Here's the revised proposal:

https://gist.github.com/jckarter/50b838e7f036fe85eaa3

For reference, here's the previous iteration:

https://gist.github.com/jckarter/f3d392cf183c6b2b2ac3

Thanks for taking a look!

-Joe


(Wallacy) #15

Just to clarify a little, this proposal is (will be) addressed to 3.0?

···

Em qui, 17 de dez de 2015 às 15:41, Joe Groff via swift-evolution < swift-evolution@swift.org> escreveu:

Hi everyone. Chris stole my thunder already—yeah, I've been working on a
design for allowing properties to be extended with user-defined
delegates^W behaviors. Here's a draft proposal that I'd like to open up
for broader discussion. Thanks for taking a look!

-Joe

https://gist.github.com/jckarter/f3d392cf183c6b2b2ac3


(Jacob Bandes-Storch) #16

Here are some of my thoughts from reading through the current version (
https://gist.github.com/jckarter/50b838e7f036fe85eaa3). Apologies if this
duplicates what anyone else has said; the thread is kind of unmanageable :slight_smile:

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

This seems like an important feature, but the syntax is strange to me. It
looks like it would be declaring storage inside the behavior, but it's
really specifying the type of something used in the containing object's
property declaration.

I can think of a couple alternatives:

1. Rather than passing an initial value in the containing object like "var
[lazy] prop = someInitialValue", pass it explicitly as a parameter to the
behavior,
    like "var [ lazy({ return someInitialValue }) ] prop: Type"

    I think it might be generally useful for behaviors' initializers to
take arguments; it'd handle more than just this case. For example, you
could have a behavior called synchronized(maxConcurrentRequests: Int) which
would allow arguments passed to affect the behavior's...behavior.

2. Make the "deferred"-ness a modifier/attribute on the behavior
declaration, like "public @deferred var behavior lazy<Value>: Value { ...
}", which would make the implicit initialValue inaccessible from the
behavior's init(). The same with @eager.

x.lazy.clear() // Invokes `lazy`'s `clear` method

As I think others have mentioned, this is ambiguous if x itself has a
property called "lazy". I'd be reasonably satisfied with any of the
proposed solutions to this.

  base var value: Int

I don't think I like the fact that this needs to be explicitly declared. Do
all behaviors have to use the same identifier for them to be composable?
Could you use "super" to mean this, instead of explicitly declaring a base
property?

Accessor requirements can be made optional by specifying a default

implementation:
  mutating accessor willSet(newValue: Value) {
    // do nothing by default
  }

Up until this point, I was thinking of accessor declarations like protocol
requirements, in that they have no implementation ("accessor foo()" like
"func foo()"). I think the lack of implementation is what makes it clear
that these are requirements, not things the behavior is implementing.

So perhaps you could use the "optional" specifier to indicate that they
aren't required, rather than allowing an implementation block in the
behavior. "optional accessor foo()" would allow the behavior's
implementation to use "foo?()".

var [foo] x: Int {

    bar(myArg) { print(myArg) } // `arg` explicitly bound to `myArg`

}

Why not require a type annotation for parameters here? I recognize this
matches the current syntax of set(newValue), but it would be more flexible
if this were more like a function declaration.

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

This seems a bit vestigial. Maybe it could be allowed only when a computed
property is declared without using any behaviors.

A few more questions:

- Can a behavior's own properties/storage use other behaviors? Can a
behavior be recursive?

- What of deinitializers for behaviors? Would it be possible, for example,
to make an observable behavior whose willSet/didSet run during init and
deinit (which Swift's current property observers can't do)?

- Are accessor implementations allowed to access the "current" property
value? Currently, inside "var foo { didSet { … } }" you can access the
current value by referencing "foo".

Overall this looks great. I'm looking forward to it. :slight_smile:

Jacob Bandes-Storch


(Joe Groff) #17

Yeah, Python decorators came up as another potential model when we were discussing this internally, since it would be similarly useful to be able to concisely wrap function bodies in memoizing/logging/proxying logic. Properties were my first target, since we already have too many special-purpose features cooked into the language, and endless feature requests for new ones. I honestly haven't thought about generalization much; at first blush, I'm concerned that the details of what you'd want to plug in to in a "function behavior" is quite different from what you want in a property. Mutable properties have a bit of non-obvious internal complexity in the language model which influences this proposal, to keep copy-on-write efficient while still allowing for abstraction. That complexity probably wouldn't be necessary for other things like enum cases or function decorators.

-Joe

···

On Dec 17, 2015, at 10:26 AM, David Owens II <david@owensd.io> wrote:

The functionality looks like it is something that is definitely required to reduce the boiler-plate that Swift currently requires. However, this seems like a specific instance of a more general problem. For example, I run into a similar problem with working with other constructs, such as enums, where I want to provide specific “behaviors” for them that is just a bunch of boiler-plate code.

It seems like this proposal could be a starting place to start flush out what a macro/preprocessor/boiler-plate-reducer would look like in Swift. As such, I would like to see a syntax that could be extended beyond properties. Maybe this system is limited in scope to only allow this generation in specific contexts, like this property behavior, especially to scope this proposal down.

The short of it: I like the idea and it seems expandable to future concerns if syntax like attributes are used. And like you mentioned, these could be generate errors when used in the wrong contexts, such as lacking @behavior_function, or its equivalent.


(Joe Groff) #18

I’ve really been looking forward to this proposal and I like this idea in general. I may have comments about specific details after giving it more thought.

I have one significant concern that is worth mentioning immediately. I would consider it extremely unfortunate if a delayed property behavior was considered sufficient in support of multi-phase initialization. Using a property behavior for that purpose gives no guarantee that the value is actually initialized at some point during instance initialization. IMO this is a major flaw. A runtime error might occur when accessing a delayed property after initialization is supposed to be complete.

Delayed property behaviors may have appropriate use cases but IMO they are not an adequate substitute for something that provides stronger guarantees for the common case of multi-phase initialization.

I very strongly prefer to see direct language support for multi-phase initialization. The compiler could provide most of the initialization guarantees it does for regular let properties. It could enforce single assignment in the initializer body and could prevent the initializer body itself from reading the delayed property before assignment.

Even without any compiler support, I think implementing (delayed) as a behavior will be an improvement over the status quo. You get none of these guarantees with a `var T!` property either, and you also lose safety from some jerk resetting the property to `nil` or changing the property again after it's supposed to stop being mutated. Being a library feature also doesn't preclude a `delayed` behavior from offering diagnostics in common cases. One of our design goals for the SIL layer was to support dataflow-sensitive diagnostics like this; even though integers and arithmetic are implemented as library features, we run optimization passes that fold literal constant arithmetic down and raise errors when constant values aren't able to statically fit in their containing types. We could do something similar for a production-quality `(delayed)` implementation in the standard library.

The only guarantee that may not be possible is method calls to self during the second phase of initialization, but prior to assignment of all delayed properties (whether directly or by passing self to another instance) are potentially dangerous if they caused a read to a delayed property. The potential for error is significantly narrower with direct language support. As this is a very common use case (possibly the most common use case for delayed properties) I strongly believe it warrants direct language support.

Yeah, statically guaranteeing initialization phase order across method calls is tricky if you don't have type state or linear types, which until recently have been fairly esoteric features (though Rust is making the latter more prominent). I think implementing `delayed` as proposed still improves the static and dynamic safety of multi-phase initialization over what we have, and it doesn't shut the door to further refinement in the future.

-Joe

···

On Dec 17, 2015, at 11:12 AM, Matthew Johnson <matthew@anandabits.com> wrote:


(Joe Groff) #19

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.

About synchronized access, it seems to me that it'd be advantageous if implementing subscript in a behaviour was optional. This way you can force access of a property through a synchronized block which will help avoid races:

  var (synchronized) data: (x: Int, y: Int, z: Int)

  func test() {
    data.synchronize { (data) in
      data.x += 1
      data.y += data.x
      data.z += data.y
    }
  }

Interesting. IIUC, if a behavior didn't supply a subscript operation, then the property could *only* be accessed through its behavior interface?

As for combining behaviours... Someone should have to do the work of combining them properly by creating a new behaviour type for the combined behaviour. To make this easier syntactically, we could have a behaviour combination function that would create a combined behaviour type. That function could be overloaded to support only behaviours that are compatible with each other.

Overall, this looks very good. But there is almost nothing said about derived classes and overriding. I very often override a property just so I can add a `didSet` clause, and if `didChange` was implemented I'd use it too. I'd very much like if those property behaviours could be compatible with overriding (where it makes sense). The way I see it, the behaviour should be able tell the compiler whether the overriding accessor should either replace or be prepended or appended to the overridden closure from the base class, or if overriding is simply not allowed.

Yeah, composition of behaviors and overloading are two areas I'm still working through. Composition is tricky, since it's not scalable to require overloads for all possible M!/N! combinations of behaviors. Any decoupled composition behavior is going to have ordering problems too—lazy-before-synchronized and synchronized-before-lazy would both be accepted, but one would be broken (unless you could blacklist problematic combinations, which also seems like an exponential-order problem).

-Joe

···

On Dec 17, 2015, at 1:14 PM, Michel Fortin <michel.fortin@michelf.ca> wrote:
Le 17 déc. 2015 à 12:37, Joe Groff via swift-evolution <swift-evolution@swift.org> a écrit :


(Félix Cloutier) #20

I'm in favor of the feature too. It addresses a real shortcoming in the language and it will make the syntax more general.

With my programming background, this looks like decorators applied specifically to properties. If this is a good way to think about it, then in case we want more for Swift 4, it might be a good idea to come up with a syntax that will easily be applicable to methods, free functions, types, enums, locals or anything else that could be decorated.

Félix

···

Le 17 déc. 2015 à 13:26:49, David Owens II via swift-evolution <swift-evolution@swift.org> a écrit :

The functionality looks like it is something that is definitely required to reduce the boiler-plate that Swift currently requires. However, this seems like a specific instance of a more general problem. For example, I run into a similar problem with working with other constructs, such as enums, where I want to provide specific “behaviors” for them that is just a bunch of boiler-plate code.

It seems like this proposal could be a starting place to start flush out what a macro/preprocessor/boiler-plate-reducer would look like in Swift. As such, I would like to see a syntax that could be extended beyond properties. Maybe this system is limited in scope to only allow this generation in specific contexts, like this property behavior, especially to scope this proposal down.

The short of it: I like the idea and it seems expandable to future concerns if syntax like attributes are used. And like you mentioned, these could be generate errors when used in the wrong contexts, such as lacking @behavior_function, or its equivalent.

-David

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution