Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional iOS Architecture for SwiftUI

Yasuhiro Inami
September 30, 2020

Functional iOS Architecture for SwiftUI

This slide is English version of my talk at iOSDC Japan 2020, on Sep 21 2020.

Book: https://zenn.dev/inamiy/books/3dd014a50f321040a047

Original slide (in Japanese):
https://speakerdeck.com/inamiy/iosdc-japan-2020

Yasuhiro Inami

September 30, 2020
Tweet

More Decks by Yasuhiro Inami

Other Decks in Programming

Transcript

  1. Reactive State Machine (2016) • MVVM to Redux / Elm

    Architecture (Mealy Machine) ΁ • Reducer = (Action, State) -> (State, Output) • Output = Publisher<Action, Never> • Unidirectional dataflow for easy state management and testability • Functional Reactive Programming (FRP) for side-effects
  2. 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)
  3. Agenda Functional iOS Architecture for SwiftUI • inamiy/Harvest • pointfreeco/swift-composable-architecture

    • bow-swift/bow-arch • Common points & differences in 3 frameworks • Comparison with Web frontend: React, Elm, etc
  4. ! 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
  5. Effect Queue Management • Publisher (Observable): Stream of effects over

    time • Stream of Stream ( Publisher<Publisher<T>>) 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)
  6. 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
  7. 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) } }
  8. struct Lens<Whole, Part> { /// struct member getter let get:

    (Whole) -> Part /// struct member setter let set: (Whole, Part) -> Whole } struct Prism<Whole, Part> { /// Getter for enum case associated values let tryGet: (Whole) -> Part? /// case func (enum constructor) let build: (Part) -> Whole }
  9. /// Converts Reducer<_, ChildState> /// into Reducer<_, ParentState> func contramapS<Action,

    State, ChildState> (_ lens: Lens<State, ChildState>) // Parent to child -> Reducer<Action, ChildState> // From child -> Reducer<Action, State> // to parent /// Converts Reducer<ChildAction, _> /// into Reducer<ParentAction, _> func contramapA<Action, ChildAction, State> (_ prism: Prism<Action, ChildAction>) // Parent to child -> Reducer<ChildAction, State> // From child -> Reducer<Action, State> // to parent Contravariant: "Parent to child" changes to "child to parent"
  10. Reducer type transform and composition func combine<A, S>(reducers: [Reducer<A, S>])

    { ... } let childReducer1: Reducer<ChildAction1, ChildState1> = ... let childReducer2: Reducer<ChildAction2, ChildState2> = ... // Converts child 1 & 2's reducer types based on parent types // and combine with the same parent types. let reducer: Reducer<Action, State> = combine([ contramapS(lens1)(contramapA(prism1)(childReducer1)) contramapS(lens2)(contramapA(prism2)(childReducer2)) ])
  11. 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
  12. 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
  13. Case Paths struct User { var name: String } //

    WritableKeyPath<User, String> let keyPath = \User.name // Backslash enum Command { case rm(rf: Bool) } // CasePath<Command, Bool> let casePath = /Command.rm // Forward-slash
  14. struct CasePath<Root, Value> { // Same shape as `Prism` let

    embed: (Value) -> Root let extract: (Root) -> Value? } prefix func / <Root, Value>( embed: @escaping (Value) -> Root ) -> CasePath<Root, Value> { /* Magic inside */ } " prefix func / " Internals: Uses Swift reflection to automagically derive extract from embed (case function). No codegen needed.
  15. 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<M: Monad> • cf. inamiy/HigherKindSwift (experimental) • Comonadic UI: UI Architecture on top of Category Theory (Mathematics)
  16. 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)
  17. struct Component<S, V: View>: 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<S, V> = W<V>, then we get: body: W<V> -> V ɾɾɾ Comonad's extract property
  18. // NOTE: Pseudo-Swift // Monad M = UF (Context-generating compuation)

    protocol Monad[M] where Functor[M] { static func `return`<C>(_ c: C) -> M<C> // η = unit static func join<C>(_ mmc: M<M<C>>) -> M<C> // static func flatMap<C, C2>(_ f: C -> M<C2>) // -> M<C> -> M<C2> } // Comonad W = FU (Context-consuming computation) protocol Comonad[W] where Functor[W] { static func extract<D>(_ wd: W<D>) -> D // ε = counit static func duplicate<D>(_ wd: W<D>) -> W<W<D>> // static func extend<D, D2>(_ f: W<D> -> D2) // -> W<D> -> W<D2> }
  19. Comonad // NOTE: Pseudo-Swift protocol Comonad[W] where Functor[W] { static

    func extract<D>(_ wd: W<D>) -> D static func duplicate<D>(_ wd: W<D>) -> W<W<D>> } • extract: From object W<D> to consume context to output Dʢe.g. Use state to output viewʣ • duplicate: Generates all the possible futures of object
  20. duplicate in nutshell (Ex: Infinite Stream) let stream = [

    0, 1, 2, ... ] // Current infinite stream // Stream of streams: Infinite stream having // current & future states of infinite streams duplicate(stream) = [ [ 0, 1, 2, ... ], // Duplicates current `stream` [ 1, 2, 3, ... ], // Duplicate + shift [ 2, 3, 4, ... ], // Duplicate + shift 2 times [ 3, 4, 5, ... ], // Duplicate + shift 3 times ... ] // ...and infinitely many more
  21. 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
  22. Component ≅ Store Comonad struct Component<S, V: View> { ///

    Current state var state: S /// Calculates virtual view from current state let _body: (S) -> V } struct Store<S, A> { let state: S let render: (S) -> A }
  23. Actual Component (with Event Handler) struct Component<S, V: View> {

    /// Current state var state: S /// Calculates virtual view /// from current state AND event handler let view: (S) -> (EventHandler) -> V } typealias EventHandler = (Action) -> IO<Void> /* Effect */
  24. Let typealias UI<V> = (EventHandler) -> V Then we get:

    struct Component<S, V: View> { var state: S let view: (S) -> UI<V> } Component<S, V> can now be expressed as Store<S, UI<V>>.
  25. 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<A> { indirect enum Shift<A> { case cons(A, Stream<A>) case done(A) } case shift(Shift<A>) } Q. What is the relationship between these two?
  26. 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<A, B, C>(f: (A, B) -> C) -> F<A> -> G<B> -> C } extension Pairing[Shift, Stream] { static func pair<A, B, C>(f: (A, B) -> C) -> Shift<A> -> Stream<B> -> 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) } } }
  27. By using Pairing, monad query can select the future of

    comonad. /// Selects future of `stream` from `shift`. func select<B>(shift: Shift<()>, stream: Stream<B>) -> Stream<B> { pair({ _, stream in stream })(shift)(duplicate(stream)) } /// In general, any monad query can select /// the future of pairing comonad. func select<M, W, B>(monad: M<()>, comonad: W<B>) -> W<B> where Monad[M], Comonad[W], Pairing[M, W] { pair({ _, comonad in comonad })(monad)(duplicate(comonad)) }
  28. For SwiftUI's Component (Store comonad), State monad can be used

    as monad query (explained later). struct Store<S, A> { let state: S let render: (S) -> A } struct State<S, A> { let runState: S -> (A, S) } extension Pairing[State, Store] { ... }
  29. Stream comonad → Shift monad Store comonad → State monad

    Q. How do we find the pairing Monad from Comonad?
  30. // Right Kan Lift of Identity functor along comonad `w`.

    struct Co<W, A> { let runCo<R>: W<A -> R> -> R } // `Co<W>` will be the derived monad for any comonad `W`. extension Monad[Co<W>] where Comonad[W] { static func `return`<C>(_ c: C) -> Co<W, C> { Co { wf in W.extract(wf)(c) } } static func join<C>(_ mmc: Co<W, Co<W, C>>) -> Co<W, C> Co { (wc2r: W<C -> R>) in mmc.runCo( W.extend({ wc2r in { (mc: Co<W, C>) in mc.runCo(wc2r) } })(wc2r) ) } } }
  31. Using Co<W, A> gives Co<Store<S>, A> ≅ State<S, A>. Proof:

    Co<W, A> ≅ ∀R. W<A -> R> -> R // Definition Co<Store<S>, A> ≅ ∀R. Store<S, A -> 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<S, A>
  32. Summary (Comonadic UI) • Comonad extract generates virtual view from

    state • Comonad duplicate makes possible futures of comonad • struct Co<W, A> creates state-updating monad query from comonad W • func select picks one of the future of comonad by using pairing monad query
  33. Comonad × Side Effects (IO) // Wrapper of comonad to

    `select`, mutate comonad reference, and run effects class EffectComponent<W, V>: ObservableObject where Comonad[W] { @Published var comonad: W<UI<V>> func explore() -> V { W.extract(comonad) { (action: IO<Co<W, Void>>) in action.flatMap { (monad: Co<W, Void>) in let nextComonad = select(monad, self.comonad.duplicate()) return IO<Void>.invoke { self.comonad = nextComonad } } } } }
  34. Comonad × Effects = Object-Oriented Programming • EffectComponent<W, V> 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
  35. Moore Comonad indirect enum Moore<I, A> { // I =

    Input, A = Output // Current output and "input to future comonad" case runMoore(A, I -> Moore<I, A>) } Same type as ∃S. (initial: S, reducer: S -> I -> S, render: S -> A), also known as Moore State Machine. Example: Elm Architecture, Redux
  36. Cofree Comonad // Comonad that can be built from any

    functor `F`. // `F(X) = I -> X` will be Moore, `F(X) = ()` will be `∃S.Store<S>` indirect enum Cofree<F, A> { case runCofree(A, F<Cofree<F, A>>) } Free Monad (Free<F, A>) will be the pairing query DSL. Example: PureScript Halogen
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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