Hello Swift Evolution Please have a look and tell me what you think. note: it’s a bit long.
Like many others "property behaviours" was something that I found quite interesting, but what got me really peaked my interest was what it could do. So now as it's been deferred for a while I would like to either resurrect it or talk about a different solution. The idea I've had is rather different, but also really similar, I'm no expert at all but I think the internal implementation would be quite similar. So as with the proposal doc of SE-0030 I'll be going through prodominantly use cases with explanations as we go in this rough sketch. property behaviours proposal: https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md property behaviours thread: https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151214/003148.html ------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------- # Type (Wrappers|Behaviours) // Type Wrapper is more explanatory of what it does, but Type Behaviour sounds cooler :) This feature doesn't require any new 'explicit' functionality be added to Swift, but rather adds sugar to certain marked types that conform to a behaviour protocol. I think it would be best to explain the sugar as they appear in the examples ``` protocol Behaviour { associatedtype InitType associatedtype SetterType associatedtype GetterType init(_ initialValue: @autoclosure @escaping () -> InitType) mutating func set(_ value: SetterType) mutating func get() -> GetterType } ``` The sugared syntax is added below code usage. All the sample code works in Swift 3.1 # Some Informalish Rules: 1. A type must conform to the Behaviour protocol to be used as a behaviour 2. A type will have to be explicitly defined at the use site, as a behaviour, to be treated as one. It will be marked in with some syntax, such as, ~BehaviourConformingType<T> 3. From (2) a type can still be used as a normal type 4. 'Behaviour' types can be used anywhere a 'normal' type can be used and is represent internally as a 'normal' type # Examples ## First Example Lazy: ``` struct Lazy<Value> : Behaviour { typealias InitType = Value typealias SetterType = Value typealias GetterType = Value var value: Value? private var initialValue: () -> Value init(_ initialValue: @autoclosure @escaping () -> Value) { value = nil self.initialValue = initialValue } mutating func get() -> Value { guard let result = value else { let initial = initialValue() value = initial return initial } return result } mutating func set(_ value: Value) { self.value = value } } print("-----------------------------Lazy") var l = Lazy(10) print(l) print(l.get()) print(l) ``` Sugar: [1.][2.] var number: ~Lazy = 10 [3.] print(number as ~Lazy) [4.] print(number) [3.] print(number as ~Lazy) 1. Initializers are inferred. 2. Generic parameters are also inferred for Behaviours. If they can't then we can use ~Lazy<Int> as an example here 3. Getting the wrapping object is done with a cast // returns Lazy<Int>. will be a compile time error if it's not a Lazy 'behaviour' 4. When a 'Behaviour' object is called normally it's get() is called ## Second Example Observed ``` struct Observed<Value> : Behaviour { typealias InitType = Value typealias GetterType = Value typealias SetterType = Value private var value: Value /* @accessor */ var willSet: (Value) -> () = { _ in } /* @accessor */ var didSet: (Value) -> () = {_ in } init(_ initialValue: @autoclosure @escaping () -> Value) { value = initialValue() } func get() -> Value { return value } mutating func set(_ value: Value) { willSet(value) let oldValue = self.value self.value = value didSet(oldValue) } } print("-----------------------------Observer") var o = Observed(10) o.didSet = { old in print("I changed:", old, "to", o.get()) } o.willSet = { new in print("I will change:", new, "to", o.get()) } o.set(5) print(o.get()) ``` Sugar: var o: Observed = 10 [1.] o.didSet = { old in print("I changed:", old, "to", o) } [1.] o.willSet = { new in print("I will change:", new, "to", o) } [2.] o = 5 1. didSet and willSet are only available directly becuase they have been marked with @accessor 2. directly setting an object calls the behaviours set method ## Third Example ChangeObserver ``` struct ChangeObserver<Value: Equatable> : Behaviour { typealias InitType = Value typealias GetterType = Value typealias SetterType = Value private var value: Value /* @accessor */ var willChange: (Value) -> () = { _ in } /* @accessor */ var didChange: (Value) -> () = {_ in } init(_ initialValue: @autoclosure @escaping () -> Value) { value = initialValue() } func get() -> Value { return value } mutating func set(_ value: Value) { let oldValue = self.value if self.value != value { willChange(value) self.value = value didChange(oldValue) } } } print("-----------------------------Change Observer") var co = ChangeObserver(1) co.willChange = { new in print("new value will be:", new) } co.didChange = { old in print("old value was:", old) } co.set(1) co.set(5) ``` Sugar: var co: ~ChangeObserver = 1 co.willChange = { new in print("new value will be:", new) } co.didChange = { old in print("old value was:", old) } co = 1 co = 5 #. Nothing new here just showing for completeness ## Fourth Example Sychronized Property Access ``` func with<R>(lock: AnyObject, body: () -> R) -> R { objc_sync_enter(lock) defer { objc_sync_exit(lock) } return body() } final class Synchronized<Value> : Behaviour { typealias InitType = Value typealias GetterType = Value typealias SetterType = Value private var value: Value init(_ initialValue: @autoclosure @escaping () -> Value) { value = initialValue() } func get() -> Value { return with(lock: self) { return value } } func set(_ value: Value) { with(lock: self) { self.value = value } } } print("-----------------------------Synchronized Property Access") func fibonacci(_ n: Int) -> Int { if n < 2 { return 1 } return fibonacci(n - 2) + fibonacci(n - 1) } var spa = Synchronized(1) DispatchQueue(label: "queueueueue1").async { spa.set(fibonacci(40)) print("fib(40): ", spa.get()) } DispatchQueue(label: "queueueueue2").async { spa.set(fibonacci(1)) print("fib(1): ", spa.get()) } ``` Sugar: var spa: ~Synchronized = 1 spa = 1 DispatchQueue(label: "queueueueue1").async { spa = fibonacci(40) print("fib(40): ", spa) } DispatchQueue(label: "queueueueue2").async { spa = fibonacci(1) print("fib(1): ", spa) } #. Again nothing new just showing another use ## Fifth Example Copying //---------------------------------------------------------NSCopying ``` struct Copying<Value: NSCopying> : Behaviour { typealias InitType = Value typealias GetterType = Value typealias SetterType = Value var value: Value init(_ initialValue: @autoclosure @escaping () -> Value) { value = initialValue().copy() as! Value } func get() -> Value { return value } mutating func set(_ value: Value) { self.value = value.copy() as! Value } } final class Point : NSCopying, CustomStringConvertible { var x, y: Int init(x: Int, y: Int) { (self.x, self.y) = (x, y) } func copy(with zone: NSZone? = nil) -> Any { return type(of: self).init(x: x, y: y) } var description: String { return "(\(x), \(y))" } } print("-----------------------------NSCopying") let p = Point(x: 1, y: 1) let q = Point(x: 2, y: 2) var a = Copying(p) var b = Copying(q) a.set(b.get()) a.value.x = 10 print(a.get()) print(b.get()) ``` Sugar: let p = Point(x: 1, y: 1) let q = Point(x: 2, y: 2) var a: ~Copying = p var b: ~Copying = q a = b a.x = 10 print(a) print(b) #. Another example, nothing new ## Sixth Example ``` //---------------------------------------------------------Reference final class Reference<Value> : Behaviour { typealias InitType = Value typealias SetterType = Value typealias GetterType = Reference<Value> var value: Value init(_ initialValue: @autoclosure @escaping () -> Value) { value = initialValue() } [1.] func get() -> Reference<Value> { return self } func set(_ value: Value) { self.value = value } } print("-----------------------------Reference") var refa = Reference(10) var refb = refa.get() refa.set(10) print(refa.get().value, "==", refb.get().value) ``` Sugar: var refa: ~Reference = 10 var refb = refa refa = 10 print(refa.value, "===", refb.value) 1. Okay theres a bit to this namely as stated above 'Behaviours' can be used as normal types such as used here with the getter returning a "Reference", note the difference between "Reference" and "~Reference", this is where, I think, the beauty of this solution comes in as 'Behaviour' types are just types with getter, setter and init sugar. ## Seventh Example Timed Functions //---------------------------------------------------------Timed ``` struct Timed<InputType, ReturnType> : Behaviour { [1.] typealias InitType = (InputType) -> ReturnType [1.] typealias SetterType = (InputType) -> ReturnType [1.] typealias GetterType = (InputType) -> (TimeInterval, ReturnType) var value: (InputType) -> ReturnType init(_ initialValue: @autoclosure @escaping () -> InitType) { value = initialValue() } func get() -> GetterType { return { input in let start = Date() let result = self.value(input) let time = Date().timeIntervalSince(start) return (time, result) } } mutating func set(_ value: @escaping SetterType) { self.value = value } } func compareTimes<T, U>(for ops: [Timed<T, U>], against value: T) { for op in ops { let (t, r) = op.get()(value) print("Time it took to calculate", r, "was", t, "ms") } } print("-----------------------------Timed") let fib = Timed(fibonacci) let fact = Timed(factorial) compareTimes(for: [fib, fact], against: 16) ``` Sugar: func compareTimes<T, U>(for ops: [2.] [~Timed<T, U>], against value: T) { for op in ops { let (t, r) = op(value) print("Time it took to calculate", r, "was", t, "ms") } } let fib: ~Timed = fibonacci let fact: ~Timed = factorial compareTimes(for: [fib, fact], against: 16) 1. An example of function wrapping 2. Showing how 'Behaviour' types can be used as parameters # Tentative ## Composition A type can be wrapped by multiple behaviours and act in the order of appearance such as ``` let a: ~T<~U<Int>> = 10 print(a) // wil be equivalant to ((a as ~T).get() as ~U).get() ``` this is for me in a tentative position because even that simple example is rather confusing, so possible making a single behaviour that has the multiple behaviours you require should be done seperatly instead ## Another Benefit? Another benefit of modeling the system this way gives us 'free' features whenever classes, structs and even enums (possible any types that can conform to a protocol? so tuples in the future?) get new features. _______________________________________________ swift-evolution mailing list swift-evolution@swift.org https://lists.swift.org/mailman/listinfo/swift-evolution