Last active
March 14, 2018 07:22
-
-
Save lukevanin/4cc51c1644c3481cb3a416c8204b01b3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ============================================================================ | |
// | |
// Quick sketch / rough idea of a redux store/state/action/reducer pattern. | |
// | |
// This works differently to ReduxJS, in that the reducer does not implement a | |
// switch statement for the action, but instead uses a double-dispatch | |
// functional call from the action to a factory that produces the redicer. | |
//. The store also has methods for each action type. | |
// | |
import Foundation | |
import PlaygroundSupport | |
// Defines a generic reducer that takes a state, and returns a modified state. | |
typealias Reducer<State> = (State) -> State | |
// Mutate a value type, using copy-on-write semantics of Swift to create a copy of the data. | |
func mutate<T>(_ t: T, f: (inout T) -> Void) -> T { | |
var u = t | |
f(&u) | |
return u | |
} | |
// App state model, composed of many sub-states. | |
struct AppState: CustomStringConvertible { | |
var employees: EmployeesState | |
var description: String { | |
return "<App employees=\(employees)>" | |
} | |
} | |
// Sub-state for employees. | |
struct EmployeesState: CustomStringConvertible { | |
var employees: [String : EmployeeState] | |
var description: String { | |
return "<Employees=\(employees)>" | |
} | |
} | |
// Sub-state for a specific employee. | |
// Note: ID is immutable as it is used as the index key. | |
struct EmployeeState: CustomStringConvertible { | |
let id: String | |
var name: String | |
var description: String { | |
return "<Employee name=\(name)" | |
} | |
} | |
// Action for adding an employee. | |
struct AddEmployeeAction { | |
let id: String | |
let name: String | |
func reducer(factory: (AddEmployeeAction) -> Reducer<AppState>) -> Reducer<AppState> { | |
return factory(self) | |
} | |
} | |
// Factory that produces app state reducers for actions. | |
struct AppStateReducerFactory { | |
let employees: EmployeesStateReducerFactory | |
func addEmployee(action: AddEmployeeAction) -> Reducer<AppState> { | |
return { [employees] in | |
mutate($0) { | |
$0.employees = employees.addEmployee(action: action)($0.employees) | |
} | |
} | |
} | |
} | |
// Factory that produces employees state reducers for actions. | |
struct EmployeesStateReducerFactory { | |
func addEmployee(action: AddEmployeeAction) -> Reducer<EmployeesState> { | |
return { | |
mutate($0) { | |
$0.employees[action.id] = EmployeeState(id: action.id, name: action.name) | |
} | |
} | |
} | |
} | |
// Helper class to encapsulate a callback function reference for an observable. | |
// The callback function is executed on the specified queue. | |
// Optionally also holds a reference to the class being observed. | |
final class ObserverCallback<T> { | |
typealias Observer = (T) -> Void | |
typealias Destroy = () -> Void | |
var destroy: Destroy? | |
private let owner: Any? | |
private let queue: DispatchQueue | |
private let observer: Observer | |
init(owner: Any? = nil, queue: DispatchQueue, observer: @escaping Observer) { | |
self.owner = owner | |
self.queue = queue | |
self.observer = observer | |
} | |
deinit { | |
destroy?() | |
} | |
func notify(_ t: T) { | |
queue.async { [observer] in | |
observer(t) | |
} | |
} | |
} | |
// Helper class that holds a weak reference to an object instance. This allows the object to be accessed | |
// by one class, while the lifetime is controlled by a different class. | |
// Used to hold references to observables, so that they can be released and removed automatically when the | |
// observable reference is deallocated. | |
final class WeakReference<T> where T: AnyObject { | |
weak var value: T? | |
init(_ value: T) { | |
self.value = value | |
} | |
} | |
// Helper class implementing an observable pattern. | |
final class Observable<T> { | |
typealias Observer = (T) -> Void | |
private var observers = [WeakReference<ObserverCallback<T>>]() | |
private let lockQueue = DispatchQueue(label: "observable-queue") | |
func observe(owner: Any?, queue: DispatchQueue, observer: @escaping Observer) -> Any { | |
let callback = ObserverCallback(owner: owner, queue: queue, observer: observer) | |
let reference = WeakReference(callback) | |
callback.destroy = { [weak self, lockQueue] in | |
lockQueue.async { | |
self.map { $0.observers = $0.observers.filter { $0.value != nil } } | |
} | |
} | |
lockQueue.sync { observers.append(reference) } | |
return callback | |
} | |
func notify(_ t: T) { | |
lockQueue.async { [weak self] in | |
self.map { | |
$0.observers.forEach { $0.value?.notify(t) } | |
$0.observers = $0.observers.filter { $0.value != nil } | |
} | |
} | |
} | |
} | |
class Store<State> { | |
typealias StateObserver = (State) -> Void | |
private var state: State | |
private let lockQueue: DispatchQueue | |
private let taskQueue: DispatchQueue | |
private var observable: Observable<State> | |
init(state: State) { | |
self.state = state | |
self.observable = Observable() | |
self.lockQueue = DispatchQueue(label: "reducer-lock") | |
self.taskQueue = DispatchQueue(label: "reducer-task") | |
} | |
func getState() -> State { | |
return lockQueue.sync { | |
return self.state | |
} | |
} | |
func setState(state: State) { | |
lockQueue.async { [weak self] in | |
self?.state = state | |
self?.observable.notify(state) | |
} | |
} | |
func observe(queue: DispatchQueue = .main, observer: @escaping StateObserver) -> Any? { | |
return observable.observe(owner: self, queue: queue, observer: observer) | |
} | |
internal func async(reducer: @escaping Reducer<State>) { | |
taskQueue.async { [weak self] in | |
guard let state = self?.getState() else { | |
return | |
} | |
let newState = reducer(state) | |
self?.setState(state: newState) | |
} | |
} | |
} | |
final class AppStore: Store<AppState> { | |
private let factory: AppStateReducerFactory | |
init(factory: AppStateReducerFactory) { | |
self.factory = factory | |
super.init(state: | |
AppState( | |
employees: EmployeesState( | |
employees: [:] | |
) | |
) | |
) | |
} | |
func dispatch(action: AddEmployeeAction) { | |
async(reducer: factory.addEmployee(action: action)) | |
} | |
} | |
let store = AppStore( | |
factory: AppStateReducerFactory( | |
employees: EmployeesStateReducerFactory() | |
) | |
) | |
print("Initial state: \(store.getState())") | |
let stateObserver = store.observe() { state in | |
print("Updated state: \(state)") | |
} | |
print("Adding employee: John Appleseed") | |
store.dispatch(action: AddEmployeeAction(id: "john_appleseed", name: "John Appleseed")) | |
print("Adding employee: Peter Pan") | |
store.dispatch(action: AddEmployeeAction(id: "peter_pan", name: "Peter Pan")) | |
PlaygroundPage.current.needsIndefiniteExecution = true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment