Skip to content

Instantly share code, notes, and snippets.

@lukevanin
Last active March 14, 2018 07:22
Show Gist options
  • Save lukevanin/4cc51c1644c3481cb3a416c8204b01b3 to your computer and use it in GitHub Desktop.
Save lukevanin/4cc51c1644c3481cb3a416c8204b01b3 to your computer and use it in GitHub Desktop.
//
// ============================================================================
//
// 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