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 full-size slide

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

    View full-size slide

  3. 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 full-size slide

  4. 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 full-size slide

  5. SwiftUI
    Combine

    View full-size slide

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

    View full-size slide

  7. 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 full-size slide

  8. !
    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 full-size slide

  9. 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 full-size slide

  10. 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 full-size slide

  11. Optics
    (Lens & Prism)

    View full-size slide

  12. 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 full-size slide

  13. 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 full-size slide

  14. Use Lens & Prism for
    Reducer
    transformation
    and composition

    View full-size slide

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

    View full-size slide

  16. /// 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 full-size slide

  17. 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 full-size slide

  18. 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 full-size slide

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

    View full-size slide

  20. Composable
    Architecture

    View full-size slide

  21. 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 full-size slide

  22. 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 full-size slide

  23. 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 full-size slide

  24. For more information, see also this session

    View full-size slide

  25. 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 full-size slide

  26. Comonadic UI

    View full-size slide

  27. 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 full-size slide

  28. 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 full-size slide

  29. // 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 full-size slide

  30. 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 full-size slide

  31. 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 full-size slide

  32. 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 full-size slide

  33. 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 full-size slide

  34. 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 full-size slide

  35. 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 full-size slide

  36. 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 full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

  39. 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 full-size slide

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

    View full-size slide

  41. // 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 full-size slide

  42. 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 full-size slide

  43. 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 full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

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

    View full-size slide

  47. 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 full-size slide

  48. 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 full-size slide

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

    View full-size slide

  50. Comonad
    defines
    Architecture

    View full-size slide

  51. 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 full-size slide

  52. 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 full-size slide

  53. 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 full-size slide

  54. 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 full-size slide

  55. 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 full-size slide

  56. Thanks!
    Yasuhiro Inami
    @inamiy

    View full-size slide