[swift-evolution] [Pitch] Type Behaviors
Letanyan Arumugam
letanyan.a at gmail.com
Thu Jun 22 01:00:49 CDT 2017
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.
More information about the swift-evolution
mailing list