Hi!
I am writing state memory system which allows user to observe mutation from state.
Let's model simplest state using
struct State {
var left = Counter()
var right = Counter()
}
struct Counter {
var value = 0
}
This state can be changed in a controlled environment. For example:
class Store {
var state = State()
func incrementLeft() {
state.left.value += 1
}
}
Now I want to allow users observer this state:
class Store {
var observers: [Observer] = []
func incrementLeft() {
state.left.value += 1
for observer in observers { observer.notify(state) }
}
}
This works great but all observers will receive all updates.
I want to notify observer only if the portion of the state that this observer did read was mutated.
func incrementLeft() {
state.left.value += 1
for observer in observers where observer.keyPaths.contains(\.left.value) {
observer.notify(state)
}
}
To collect information about accessed properties I want to use @dynamicMemberLookup capabilities:
class Reader<T> {
init(value: T) { self.value = value}
let value: T
let accessedKeyPaths = [] as Set<PartialKeyPath<T>>
subscript<V>(dynamicMember keyPath: KeyPath<T, V>) -> V {
accessedKeyPath.insert(keyPath)
return value[keyPath: keyPath]
}
}
This concept works great on flat structures, but nesting causes a lot of problems.
- Accessing any value inside a counter will not be recorded. To mitigate this I can add overload
subscript<V>(dynamicMember keyPath: KeyPath<T, V>) -> Reader<V> {
...
}
But then swift is struggling to disambiguate between them.
Alternatively I can mark all terminal types with special marker protocol, and add value resolution only for them, but this idea doesn't looks scalable for me.
I also tried special operator and function to actually fetch data from readers, but all this is not seamless enough.
Maybe there is a way to nudge Swift to choosing Reader overload over regular one?