Functional iOS Architecture for SwiftUI

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

Eac0bf787b5279aca5e699ece096956e?s=128

Yasuhiro Inami

September 30, 2020
Tweet

Transcript

  1. Functional iOS Architecture for SwiftUIɹɹɹ 2020/09/21 iOSDC Japan 2020 (translated

    in English) Yasuhiro Inami / @inamiy
  2. ɹhttps://speakerdeck.com/inamiy/reactive-state-machine-japanese

  3. None
  4. 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
  5. 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)
  6. None
  7. SwiftUI Combine

  8. None
  9. Published "Harvest" (successor of Reactive State Machine)

  10. 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
  11. ! Harvest

  12. ! 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
  13. 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)
  14. None
  15. None
  16. 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
  17. Optics (Lens & Prism)

  18. 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) } }
  19. 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 }
  20. None
  21. None
  22. None
  23. None
  24. None
  25. None
  26. None
  27. None
  28. None
  29. None
  30. None
  31. None
  32. None
  33. Use Lens & Prism for Reducer transformation and composition

  34. If Reducer is composable We can decompose States and Actions

    in each module
  35. /// 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"
  36. 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)) ])
  37. 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
  38. None
  39. None
  40. None
  41. Harvest-SwiftUI- Gallery https://github.com/inamiy/Harvest-SwiftUI-Gallery

  42. Composable Architecture

  43. 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
  44. None
  45. None
  46. None
  47. None
  48. None
  49. 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
  50. 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.
  51. For more information, see also this session

  52. Bow Arch

  53. 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)
  54. Comonadic UI

  55. 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)
  56. 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
  57. None
  58. None
  59. None
  60. None
  61. // 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> }
  62. 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
  63. 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
  64. 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
  65. 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 }
  66. 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 */
  67. 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>>.
  68. 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?
  69. 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) } } }
  70. 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)) }
  71. 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] { ... }
  72. Stream comonad → Shift monad Store comonad → State monad

    Q. How do we find the pairing Monad from Comonad?
  73. // 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) ) } } }
  74. 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>
  75. 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
  76. 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 } } } } }
  77. 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
  78. Q. Can EffectComponent<W, V> be used for other comonads W?

    !
  79. 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
  80. 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
  81. Comonad → Architecture Store → React Moore → Elm Cofree

    → PureScript
  82. Comonad defines Architecture

  83. None
  84. Recap

  85. 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
  86. 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
  87. 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
  88. 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
  89. 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
  90. Thanks! Yasuhiro Inami @inamiy