Last active
December 7, 2021 14:25
-
-
Save mayoff/4659e5f950a9158e80cadd63e7fec218 to your computer and use it in GitHub Desktop.
An implementation of Point-Free's Composable Architecture, with the addition of subscriptions from The Elm Architecture (TEA). See https://github.com/pointfreeco/episode-code-samples/issues/51
This file contains 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
#if canImport(Combine) | |
import Combine | |
import CasePaths | |
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) | |
public typealias Effect<Output> = AnyPublisher<Output, Never> | |
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) | |
extension Effect { | |
public static var noEffects: Effect<Output> { | |
return Empty(completeImmediately: true) | |
.eraseToAnyPublisher() | |
} | |
public static func fireAndForget(_ body: @escaping () -> Void) -> Effect<Output> { | |
let empty = Empty<Output, Never>(completeImmediately: true) | |
return Deferred { (body(), empty).1 } | |
.eraseToAnyPublisher() | |
} | |
public static func sync(_ body: @escaping () -> Output) -> Effect<Output> { | |
return Deferred { Just(body()) } | |
.eraseToAnyPublisher() | |
} | |
} | |
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) | |
public struct Reducer<Model, Action> { | |
public init(_ apply: @escaping (inout Model, Action) -> [Effect<Action>]) { | |
_apply = apply | |
} | |
public func apply(_ action: Action, to model: inout Model) -> [Effect<Action>] { | |
return _apply(&model, action) | |
} | |
public func pullback<OuterModel, OuterAction>( | |
model: WritableKeyPath<OuterModel, Model>, | |
action: CasePath<OuterAction, Action> | |
) -> Reducer<OuterModel, OuterAction> { | |
return .init { (outerModel, outerAction) -> [AnyPublisher<OuterAction, Never>] in | |
guard let innerAction = action.extract(from: outerAction) else { return [] } | |
return self | |
.apply(innerAction, to: &outerModel[keyPath: model]) | |
.map { $0.map(action.embed).eraseToAnyPublisher() } | |
} | |
} | |
private let _apply: (inout Model, Action) -> [Effect<Action>] | |
} | |
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) | |
public protocol Extrinsic: Hashable { | |
associatedtype Action | |
func publisher() -> AnyPublisher<Action, Never> | |
} | |
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) | |
fileprivate class ExtrinsicCollectionBase<Model, Action> { | |
func update(with store: RootStore<Model, Action>) { | |
fatalError("subclass responsibility") | |
} | |
} | |
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) | |
fileprivate class ExtrinsicCollection<Model, Ex: Extrinsic>: ExtrinsicCollectionBase<Model, Ex.Action> { | |
init(_ make: @escaping (Model) -> Set<Ex>) { | |
self.make = make | |
} | |
override func update(with store: RootStore<Model, Ex.Action>) { | |
let exs = make(store.model) | |
for ex in ticketForEx.keys.filter({ !exs.contains($0) }) { | |
ticketForEx.removeValue(forKey: ex)?.cancel() | |
} | |
for ex in exs.subtracting(ticketForEx.keys) { | |
ticketForEx[ex] = ex.publisher() | |
.sink(receiveValue: { [weak store] in store?.send($0) }) | |
} | |
} | |
private let make: (Model) -> Set<Ex> | |
private var ticketForEx: [Ex: AnyCancellable] = [:] | |
} | |
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) | |
@dynamicMemberLookup | |
public class Store<Model, Action>: ObservableObject { | |
public static func root( | |
withModel model: Model, | |
reducer: Reducer<Model, Action> | |
) -> Store<Model, Action> { | |
return RootStore(model: model, reducer: reducer, extrinsicCollection: nil) | |
} | |
public static func root<Ex: Extrinsic>( | |
withModel model: Model, | |
reducer: Reducer<Model, Action>, | |
extrinsics: @escaping (Model) -> Set<Ex> | |
) -> Store<Model, Action> where Ex.Action == Action { | |
return RootStore(model: model, reducer: reducer, extrinsicCollection: ExtrinsicCollection(extrinsics)) | |
} | |
public var model: Model { fatalError("subclass responsibility") } | |
public func send(_ action: Action) { fatalError("subclass responsibility") } | |
public var objectWillChange: AnyPublisher<Void, Never> { fatalError("subclass responsibility") } | |
public final func view<InnerModel, InnerAction>( | |
toInner: @escaping (Model) -> InnerModel, | |
toOuter: @escaping (InnerAction) -> Action | |
) -> Store<InnerModel, InnerAction> { | |
return InnerStore(outer: self, toInner: toInner, toOuter: toOuter) | |
} | |
public subscript<Value>(dynamicMember keyPath: KeyPath<Model, Value>) -> Value { model[keyPath: keyPath] } | |
} | |
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) | |
fileprivate final class RootStore<Model, Action>: Store<Model, Action> { | |
override var model: Model { _model } | |
override func send(_ action: Action) { | |
let effects = reducer.apply(action, to: &_model) | |
for effect in effects { | |
var ticket: AnyCancellable? = nil | |
ticket = effect.sink( | |
receiveCompletion: { _ in ticket = nil }, | |
receiveValue: { self.send($0) }) | |
withExtendedLifetime(ticket) { } | |
} | |
extrinsicCollection?.update(with: self) | |
subject.send() | |
} | |
override var objectWillChange: AnyPublisher<Void, Never> { _objectWillChange } | |
init(model: Model, reducer: Reducer<Model, Action>, extrinsicCollection: ExtrinsicCollectionBase<Model, Action>?) { | |
_model = model | |
self.reducer = reducer | |
self.extrinsicCollection = extrinsicCollection | |
_objectWillChange = subject.eraseToAnyPublisher() | |
super.init() | |
extrinsicCollection?.update(with: self) | |
} | |
private var _model: Model | |
private let reducer: Reducer<Model, Action> | |
private let subject = PassthroughSubject<Void, Never>() | |
private let _objectWillChange: AnyPublisher<Void, Never> | |
private let extrinsicCollection: ExtrinsicCollectionBase<Model, Action>? | |
} | |
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) | |
fileprivate final class InnerStore<OuterModel, OuterAction, InnerModel, InnerAction>: Store<InnerModel, InnerAction> { | |
override var model: InnerModel { toInner(outer.model) } | |
override func send(_ action: InnerAction) { outer.send(toOuter(action)) } | |
override var objectWillChange: AnyPublisher<Void, Never> { outer.objectWillChange } | |
init( | |
outer: Store<OuterModel, OuterAction>, | |
toInner: @escaping (OuterModel) -> InnerModel, | |
toOuter: @escaping (InnerAction) -> OuterAction | |
) { | |
self.outer = outer | |
self.toInner = toInner | |
self.toOuter = toOuter | |
} | |
private let outer: Store<OuterModel, OuterAction> | |
private let toInner: (OuterModel) -> InnerModel | |
private let toOuter: (InnerAction) -> OuterAction | |
} | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment