[Pitch] Type Behaviors


(Letanyan Arumugam) #1

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 :slight_smile:
  
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.