Skip to content

Instantly share code, notes, and snippets.

@daniel-hall
Last active October 28, 2019 07:05
Show Gist options
  • Save daniel-hall/422775084a05779b0157caf68955e09d to your computer and use it in GitHub Desktop.
Save daniel-hall/422775084a05779b0157caf68955e09d to your computer and use it in GitHub Desktop.
SingleWriter wraps any struct in a mechanism that only allows modifications through an optional-typed writer object. Only one writer object can exist at a time, so while one code site is referencing the writer or using it to change a property on the struct, any other code that attempts to get the writer to make changes will get a nil value.
// The SingleWriter generic type only works with structs
//
// SingleWriter wraps any struct in a mechanism that only allows modifications through an optional-typed writer object.
// Only one writer object can exist at a time, so while one code site is referencing the writer or using it to change
// a property on the struct, any other code that attempts to get the writer to make changes will get a nil value.
//
// This is a tool to work with global shared mutable state, that allows write access to be controlled across threads or
// call sites. Any code using the writer interface will cause any other code / threads to not have access to the writer
// interface.
//
// Like more traditional locking, the SingleWriter type also has a writeNext method that allows client code to add a
// write operation to a queue that will run as soon as any existing references to the writer interface are released
class SingleWriter<T> {
private var value:T
private var nextWriters = [(WriterProxy<T>)->()]()
private weak var proxy: WriterProxy<T>?
/// Returns a read-only copy of the wrapped struct
var read: T { return value }
/// Returns a writer interface that allows changes to the wrapped struct. Only one interface instance can exist at a time
var writer: WriterProxy<T>? {
get {
if self.proxy != nil { return nil }
let proxy = WriterProxy<T>(value:value, updateClosure: { self.value = $0 }){
if let next = self.nextWriters.popLast(), let writer = self.writer {
next(writer)
}
}
self.proxy = proxy
return proxy
}
}
/// Init with any struct instance to provide the single writer interface for
init(value:T) {
self.value = value
}
/// Provide a closure to write modifications to the wrapped struct whenever the writer interface is next available
func writeNext(closure:@escaping (WriterProxy<T>)->()) {
if let writer = writer {
closure(writer)
} else {
nextWriters.insert(closure, at: 0)
}
}
}
class WriterProxy<T> {
var write:T {
didSet {
updateClosure(write)
}
}
private let updateClosure:(T)->()
private let completedClosure:()->()
fileprivate init(value:T, updateClosure:@escaping (T)->(), completedClosure:@escaping ()->()) {
self.write = value
self.updateClosure = updateClosure
self.completedClosure = completedClosure
}
deinit {
completedClosure()
}
}
// Example usage:
// A simple struct type
struct User {
var name:String
var id:Int
}
// Wrap an instance of a User with initial values in a SingleWriter instance
let currentUser = SingleWriter(value: User(name: "Default", id: 0))
print(currentUser.read.name) // prints "Default"
// hold a reference to the "writer" interface for this User instance
var writerReference = currentUser.writer
// modify the name via the reference to the "writer" interface
writerReference?.write.name = "Joe"
// try to also modify the name through a different call to the "writer" interface. Because writerReference already has a
// "lock" on the "writer" interface, this attempt to get the "writer" interface returns nil and the write operation
// doesn't take effect
currentUser.writer?.write.name = "Sally"
// notice that only the first modification to the name, via the writerReference worked
print(currentUser.read.name) // prints "Joe"
// queue up a modification for when the writer interface is next released
currentUser.writeNext{ $0.write.name = "Bobo" }
// because there is still a reference to the "writer" interface being retained, the "Bobo" name change hasn't happened yet
print(currentUser.read.name) // prints "Joe"
// release the retained reference to the single "writer" interface
writerReference = nil
// as soon as the reference to the "writer" interface was released, the queued name change to "Bobo" was applied
print(currentUser.read.name) // prints "Bobo"
// and now other code can access the single "writer" interface again as well and this will work
currentUser.writer?.write.name = "Sally"
currentUser.writer?.write.id = 10
print(currentUser.read) // prints User(name: "Sally", id: 10)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment