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

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

Yasuhiro Inami

September 30, 2020
Tweet

More Decks by Yasuhiro Inami

Other Decks in Programming

Transcript

  1. Functional
    iOS Architecture
    for SwiftUIɹɹɹ
    2020/09/21 iOSDC Japan 2020 (translated in English)
    Yasuhiro Inami / @inamiy

    View Slide

  2. ɹhttps://speakerdeck.com/inamiy/reactive-state-machine-japanese

    View Slide

  3. View Slide

  4. Reactive State Machine (2016)
    • MVVM to Redux / Elm Architecture (Mealy Machine) ΁
    • Reducer = (Action, State) -> (State, Output)
    • Output = Publisher
    • Unidirectional dataflow for easy state management and
    testability
    • Functional Reactive Programming (FRP) for side-effects

    View Slide

  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)

    View Slide

  6. View Slide

  7. SwiftUI
    Combine

    View Slide

  8. View Slide

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

    View Slide

  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

    View Slide

  11. !
    Harvest

    View Slide

  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

    View Slide

  13. 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)

    View Slide

  14. View Slide

  15. View Slide

  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

    View Slide

  17. Optics
    (Lens & Prism)

    View Slide

  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)
    } }

    View Slide

  19. 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
    }

    View Slide

  20. View Slide

  21. View Slide

  22. View Slide

  23. View Slide

  24. View Slide

  25. View Slide

  26. View Slide

  27. View Slide

  28. View Slide

  29. View Slide

  30. View Slide

  31. View Slide

  32. View Slide

  33. Use Lens & Prism for
    Reducer
    transformation
    and composition

    View Slide

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

    View Slide

  35. /// Converts Reducer<_, ChildState>
    /// into Reducer<_, ParentState>
    func contramapS
    (_ lens: Lens) // Parent to child
    -> Reducer // From child
    -> Reducer // to parent
    /// Converts Reducer
    /// into Reducer
    func contramapA
    (_ prism: Prism) // Parent to child
    -> Reducer // From child
    -> Reducer // to parent
    Contravariant: "Parent to child" changes to "child to parent"

    View Slide

  36. 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))
    ])

    View Slide

  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

    View Slide

  38. View Slide

  39. View Slide

  40. View Slide

  41. Harvest-SwiftUI-
    Gallery
    https://github.com/inamiy/Harvest-SwiftUI-Gallery

    View Slide

  42. Composable
    Architecture

    View Slide

  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

    View Slide

  44. View Slide

  45. View Slide

  46. View Slide

  47. View Slide

  48. View Slide

  49. Case Paths
    struct User {
    var name: String
    }
    // WritableKeyPath
    let keyPath = \User.name // Backslash
    enum Command {
    case rm(rf: Bool)
    }
    // CasePath
    let casePath = /Command.rm // Forward-slash

    View Slide

  50. struct CasePath { // Same shape as `Prism`
    let embed: (Value) -> Root
    let extract: (Root) -> Value?
    }
    prefix func / (
    embed: @escaping (Value) -> Root
    ) -> CasePath { /* Magic inside */ }
    " prefix func / " Internals:
    Uses Swift reflection to automagically derive extract from
    embed (case function). No codegen needed.

    View Slide

  51. For more information, see also this session

    View Slide

  52. Bow Arch

    View Slide

  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
    • cf. inamiy/HigherKindSwift (experimental)
    • Comonadic UI: UI Architecture on top of Category Theory
    (Mathematics)

    View Slide

  54. Comonadic UI

    View Slide

  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)

    View Slide

  56. 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

    View Slide

  57. View Slide

  58. View Slide

  59. View Slide

  60. View Slide

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

    View Slide

  62. 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

    View Slide

  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

    View Slide

  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

    View Slide

  65. 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
    }

    View Slide

  66. 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 */

    View Slide

  67. Let
    typealias UI = (EventHandler) -> V
    Then we get:
    struct Component {
    var state: S
    let view: (S) -> UI
    }
    Component can now be expressed
    as Store>.

    View Slide

  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 { indirect enum Shift {
    case cons(A, Stream) case done(A)
    } case shift(Shift)
    }
    Q. What is the relationship between these two?

    View Slide

  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(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)
    }
    }
    }

    View Slide

  70. 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))
    }

    View Slide

  71. 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] { ... }

    View Slide

  72. Stream comonad → Shift monad
    Store comonad → State monad
    Q. How do we find the pairing
    Monad from Comonad?

    View Slide

  73. // 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)
    )
    }
    }
    }

    View Slide

  74. 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

    View Slide

  75. 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

    View Slide

  76. 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
    }
    }
    }
    }
    }

    View Slide

  77. 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

    View Slide

  78. Q. Can EffectComponent be
    used for other comonads W? !

    View Slide

  79. 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

    View Slide

  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`
    indirect enum Cofree {
    case runCofree(A, F>)
    }
    Free Monad (Free) will be the pairing query DSL.
    Example: PureScript Halogen

    View Slide

  81. Comonad → Architecture
    Store → React
    Moore → Elm
    Cofree → PureScript

    View Slide

  82. Comonad
    defines
    Architecture

    View Slide

  83. View Slide

  84. Recap

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  90. Thanks!
    Yasuhiro Inami
    @inamiy

    View Slide