Reactive State Machine (2016) • Proof of Concept • ReactiveAutomaton (ReactiveSwift) • RxAutomaton (RxSwift) • Misc • React & Elm inspired frameworks in Swift • SwiftElmɿVirtual View on top of UIKit (experimental)
! Harvest • Successor of Reactive State Machine (Elm Architecture-like) • SwiftUI + Combine support (HarvestStore) • Works as a Dependency Container • Effect cancellation support • Effect Queue Management using FRP • Optics (Lens & Prism): Modularization of State, Action, Reducer, and their compositions
Effect Queue Management • Publisher (Observable): Stream of effects over time • Stream of Stream ( Publisher>) is same as effects being enqueued by Effect Queue • Queued effects can be flattened into a single stream • FlattenStrategy for Effect Queue • merge, concat, concurrent(max:), switchLatest, race, etc (derived from ReactiveSwift)
Elm Architecture × FRP • The essence of FRP is to build a dataflow pipeline with hiding the internal states of Publisher • But more hiding will cause harder state management • Use Elm Architecture together with FRP to ignore trivial state management • Examples: Rx.throttle to memorize previous time, or Effect Queue (State) Management
Optics (Lens & Prism) • Lens: 2 operations for State (struct product type) • Example: Get user name & set user name • Prism: 2 operations for Action (enum sum type) • Example: Build command & get command's option value struct User { enum Command { var name: String case rm(rf: Bool) } }
struct Lens { /// struct member getter let get: (Whole) -> Part /// struct member setter let set: (Whole, Part) -> Whole } struct Prism { /// Getter for enum case associated values let tryGet: (Whole) -> Part? /// case func (enum constructor) let build: (Part) -> Whole }
Reducer type transform and composition func combine(reducers: [Reducer]) { ... } let childReducer1: Reducer = ... let childReducer2: Reducer = ... // Converts child 1 & 2's reducer types based on parent types // and combine with the same parent types. let reducer: Reducer = combine([ contramapS(lens1)(contramapA(prism1)(childReducer1)) contramapS(lens2)(contramapA(prism2)(childReducer2)) ])
Summary (Optics) • Optics allows us to maintain app's State (struct) and Action (enum) modular per UI component, and keeps manageable by forming a tree structure • Lens and Prism becomes important when combining each UI component's Reducers into one AppReducer (better version of react-redux) • Beauty of Optics can be found in Functional Programming and Category Theory
Composable Architecture (TCA) • Elm-like Architecture by Point-Free Team • Multi-Store Architecture: Child components communicate with parent components and reactively synchronizes states • swift-case-paths as Smart Prism • WritableKeyPath (Swift standard type) as Lens • Over ˑ2000, good documentation & video tutorials
Bow Arch • A new UI Architecture by Tomás Ruiz-López (47 Degrees) • Bow: Functional programming library using "Lightweight Higher Kinded Polymorphism" technique • Can write e.g. func foo • cf. inamiy/HigherKindSwift (experimental) • Comonadic UI: UI Architecture on top of Category Theory (Mathematics)
Comonad ≈ OOP − Mutable Reference • SwiftUI.View as Comonad • Comonad: Holds internal state and calculate output • Ex: Iterator pattern outputs next value from internal state • Ex: Builder pattern accumulates state and output • Ex: React Component (SwiftUI) owns state and render (body) VirtualDOM (View)
struct Component: View { // Current state. var state: S // Calculates virtual view from current state. let _body: (S) -> V // Note: Actual type signature is `Self -> V` var body: V { _body(state) } } Let Component = W, then we get: body: W -> V ɾɾɾ Comonad's extract property
Comonad // NOTE: Pseudo-Swift protocol Comonad[W] where Functor[W] { static func extract(_ wd: W) -> D static func duplicate(_ wd: W) -> W> } • extract: From object W to consume context to output Dʢe.g. Use state to output viewʣ • duplicate: Generates all the possible futures of object
duplicate in nutshell (Ex: SwiftUI / React) let makeComp: (S) -> Component = Component(_body: ...) // Note: Partial apply let component: Component = makeComp(state: ...) // Current Component // Component that generates all possible futures of Component duplicate(component) = Component(_body: makeComp, state: component.state) ≈ [ /* Duplicates current component */, /* Duplicate + some state changes */, /* Duplicate + another possible state changes */, ... ] // Forms a "space" of all possible states for Component
Component ≅ Store Comonad struct Component { /// Current state var state: S /// Calculates virtual view from current state let _body: (S) -> V } struct Store { let state: S let render: (S) -> A }
Actual Component (with Event Handler) struct Component { /// Current state var state: S /// Calculates virtual view /// from current state AND event handler let view: (S) -> (EventHandler) -> V } typealias EventHandler = (Action) -> IO /* Effect */
Modifying Comonad's state from outside In infinite stream example, shift (n = 0, 1, 2, l) is the state- mutating (can be expressed as monadic query) // Infinite stream (Comonad) // Shift query (Monad) indirect enum Stream { indirect enum Shift { case cons(A, Stream) case done(A) } case shift(Shift) } Q. What is the relationship between these two?
A. Stream comonad and Shift monad conform Pairing. protocol Pairing[F, G] { /// Natural transformation from Day convolution to Identity /// i.e. `Day f g ~> Identity` static func pair(f: (A, B) -> C) -> F -> G -> C } extension Pairing[Shift, Stream] { static func pair(f: (A, B) -> C) -> Shift -> Stream -> C { switch (shift, stream) { case let (.done(a), .cons(b, _)): return f(a, b) case let (.shift(nextShift), .cons(_, nextStream)): return pair(f)(nextShift)(nextStream) } } }
By using Pairing, monad query can select the future of comonad. /// Selects future of `stream` from `shift`. func select(shift: Shift<()>, stream: Stream) -> Stream { pair({ _, stream in stream })(shift)(duplicate(stream)) } /// In general, any monad query can select /// the future of pairing comonad. func select(monad: M<()>, comonad: W) -> W where Monad[M], Comonad[W], Pairing[M, W] { pair({ _, comonad in comonad })(monad)(duplicate(comonad)) }
For SwiftUI's Component (Store comonad), State monad can be used as monad query (explained later). struct Store { let state: S let render: (S) -> A } struct State { let runState: S -> (A, S) } extension Pairing[State, Store] { ... }
// Right Kan Lift of Identity functor along comonad `w`. struct Co { let runCo: W R> -> R } // `Co` will be the derived monad for any comonad `W`. extension Monad[Co] where Comonad[W] { static func `return`(_ c: C) -> Co { Co { wf in W.extract(wf)(c) } } static func join(_ mmc: Co>) -> Co Co { (wc2r: W R>) in mmc.runCo( W.extend({ wc2r in { (mc: Co) in mc.runCo(wc2r) } })(wc2r) ) } } }
Using Co gives Co, A> ≅ State. Proof: Co ≅ ∀R. W R> -> R // Definition Co, A> ≅ ∀R. Store R> -> R ≅ ∀R. (S, (S -> A -> R)) -> R ≅ S -> ∀R. (S -> A -> R) -> R // currying ≅ S -> ∀R. ((S, A) -> R) -> R // uncurrying ≅ S -> (S, A) // Yoneda Lemma: ∀R. (X -> R) -> R ≅ X ≅ State
Summary (Comonadic UI) • Comonad extract generates virtual view from state • Comonad duplicate makes possible futures of comonad • struct Co creates state-updating monad query from comonad W • func select picks one of the future of comonad by using pairing monad query
Comonad × Side Effects (IO) // Wrapper of comonad to `select`, mutate comonad reference, and run effects class EffectComponent: ObservableObject where Comonad[W] { @Published var comonad: W> func explore() -> V { W.extract(comonad) { (action: IO>) in action.flatMap { (monad: Co) in let nextComonad = select(monad, self.comonad.duplicate()) return IO.invoke { self.comonad = nextComonad } } } } }
Comonad × Effects = Object-Oriented Programming • EffectComponent is an "object" in OOP, separating the concerns into "comonad" and "side-effect" • If W = Store comonad, State monad querying with side- effects means calling React's setState • Equivalent to SwiftUI's @State mutation • Note: Components can be nested using comonad transformer
Moore Comonad indirect enum Moore { // I = Input, A = Output // Current output and "input to future comonad" case runMoore(A, I -> Moore) } Same type as ∃S. (initial: S, reducer: S -> I -> S, render: S -> A), also known as Moore State Machine. Example: Elm Architecture, Redux
Cofree Comonad // Comonad that can be built from any functor `F`. // `F(X) = I -> X` will be Moore, `F(X) = ()` will be `∃S.Store` indirect enum Cofree { case runCofree(A, F>) } Free Monad (Free) will be the pairing query DSL. Example: PureScript Halogen
Recap • The essence of SwiftUI is Comonad • Comonad structure defines UI architecture patterns • SwiftUI, React, Elm, PureScript Halogen, etc... • Optics for modularizing states, actions, reducers, and combine them all in an elegant way • Functional Programming (and Category Theory): A tool for understanding the essence of programming
Harvest TCA Bow Arch GitHub Stars ˑ 300 ˑ 2000 ˑ 100 Author's activity ❓ Difficulty Medium Medium Hard Effects Combine Combine BowEffects Optics FunOptics WritableKeyPath & CasePaths BowOptics Comonadic UI Moore Moore Any Comonads Inspired from Elm Elm & React PureScript & Category Theory
References (Libraries & Optics) • ! Harvest: Apple's Combine.framework + State Machine, inspired by Elm • Composable Architecture • bow-arch: Comonadic UIs • Brandon Williams - Lenses in Swift • Lenses and Prisms in Swift: a pragmatic approach | Fun iOS
References (Comonadic UI) • Declarative UIs are the Future — And the Future is Comonadic! • The Future Is Comonadic! - Speaker Deck • Comonads for user interfaces - Arthur Xavier • A Real-World Application with a Comonadic User Interface • The Comonad.Reader » Monads from Comonads
References (My related old talks) • ! Reactive State Machine • ! Reactive State Machine - iOS Conf SG 2016 - YouTube • " Make Elm Architecture in Swift • ! React & Elm inspired frameworks in Swift • " Higher Kinded Types in Swift • " Category Theory in Swift / iOSDC Japan 2018 • " Category Theory and Programming