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

Understanding the principles of Rx, React, Flow...

Avatar for Victor Lai Victor Lai
September 26, 2022

Understanding the principles of Rx, React, Flow through Programming Paradigms

Presented at Droidcon NYC 2022

This talk introduces the concept of programming paradigms, explores the use of declarative programming, explains the concepts behind reactive and functional reactive programming, and then discusses how to use declarative-ness to improve our programming abstractions.

Avatar for Victor Lai

Victor Lai

September 26, 2022
Tweet

Other Decks in Programming

Transcript

  1. what to expect 1. Table of Contents 2. Overview 3.

    What is a computer? 4. Programming paradigms 5. Strong abstractions
  2. what to expect • My goal is to give you

    a different lens in which to view programming
  3. what to expect • My goal is to give you

    a different lens in which to view programming • Also, to get you excited about programming paradigms
  4. what to expect • My goal is to give you

    a different lens in which to view programming • Also, to get you excited about programming paradigms • Not academically rigorous, except where noted
  5. overview fun main() { val x = calc() } fun

    func(): Int { val a = 1 val b = 2 return a + b }
  6. overview fun main() { val x = calc() } fun

    func(): Int { val a = 1 val b = 2 return a + b } Functions? Imperative?
  7. overview fun main() { val x = calc() } fun

    func(): Int { val a = 1 val b = 2 return a + b } Functions? Imperative? How can we have imperative and functional code at the same time?
  8. overview • How do we draw the lines between even

    imperative and functional programming?
  9. overview • How do we draw the lines between even

    imperative and functional programming? • How we even vocalize the differences in style in the same block of code?
  10. • How do we draw the lines between even imperative

    and functional programming? • How we even vocalize the differences in style in the same block of code? • What is Reactive Programming? What is Functional Reactive Programming? overview
  11. • How do we draw the lines between even imperative

    and functional programming? • How we even vocalize the differences in style in the same block of code? • What is Reactive Programming? What is Functional Reactive Programming? • Is there a difference? Does it even matter? overview
  12. what is a computer? • How do you build a

    logic gate from transistors?
  13. what is a computer? • How do you build a

    logic gate from transistors?
  14. what is a computer? • How do you build a

    logic gate from transistors?
  15. what is a computer? • How do you build a

    logic gate from transistors?
  16. what is a computer? • Memory, latches PC - Program

    Counter MAR - Memory Address Register RAM - Random Access Memory IR - Instruction Register CU - Control Unit ACC - Accumulator ALU - Arithmetic Logic Unit B - B register
  17. what is a computer? • Memory, latches 1. PC is

    0000 2. IR loads instruction at address 0000 3. CU executes instruction 4. PC increments
  18. programming paradigms • Why learn about computers? • Silicon •

    Transistors • Logic gates • Synchronous circuits • Instruction set • Programming language
  19. programming paradigms • Layers of abstraction • Silicon • Transistors

    • Logic gates • Synchronous circuits • Instruction set • Programming language
  20. programming paradigms • Layers of abstraction • Programming languages •

    Refine: abstractions, paradigms • Increase: ??, no code?, spreadsheets
  21. programming paradigms • Silicon • Transistors • Logic gates •

    Synchronous circuits • Instruction set • Programming language • Programming abstractions, paradigms
  22. programming paradigms • Why learn about computers? • Computers are

    imperative • Every computer is an abstraction on top of an instruction set for a synchronous circuit
  23. programming paradigms • Computers are imperative • Imperative is the

    baseline • Declarative is everything else • Re-think what it means to be declarative
  24. programming paradigms • Re-think what it means to be declarative

    • Declarative means to declare the computer’s (imperative) behavior • But fundamentally still backed by imperative abstractions • More declarative hides implementation details, aids fuller understanding
  25. programming paradigms fun main() { val x = calc() }

    fun func(): Int { val a = 1 val b = 2 return a + b } Functional? Imperative?
  26. programming paradigms fun main() { val x = calc() }

    fun func(): Int { val a = 1 val b = 2 return a + b } Imperative details
  27. programming paradigms fun main() { val x = calc() }

    fun func(): Int { val a = 1 val b = 2 return a + b } Declarative functions
  28. programming paradigms fun main() { val x = calc() }

    fun func(): Int { val a = 1 val b = 2 return a + b } Declarative functions Imperative details
  29. programming paradigms fun main() { val x = calc() }

    fun func(): Int { val a = 1 val b = 2 return a + b } Declarative functions Imperative details Different declarative-ness depending on how we view the abstraction
  30. programming paradigms fun main() { val x = calc() }

    fun func(): Int { val a = 1 val b = 2 return a + b } Declarative functions Imperative details Think interface versus implementation detail
  31. programming paradigms • Declarative is a gradient • More declarative

    depending on what level of abstraction we’re looking at the code with
  32. programming paradigms • Object-Oriented Programming is imperative? • But OOP

    “declares” that objects have functionality • Functionality between objects still needs to be wired together, imperatively
  33. programming paradigms • Object-Oriented Programming is imperative? • But OOP

    “declares” that objects have functionality • Functionality between objects still needs to be wired together, imperatively • Functionality of the object can be whatever, as long as the declared contract is fulfilled
  34. programming paradigms • We invent rules to improve the declarative-

    ness of OOP, e.g. SOLID principles • Single Responsibility • Open Closed • Liskov Substitution • Interface Separation • Dependency Inversion
  35. programming paradigms • We invent rules to improve the declarative-

    ness of OOP, e.g. SOLID principles • Single Responsibility • The less an object has to do, the better/ safer/easier its behavior can be declared, and reused
  36. programming paradigms • We invent rules to improve the declarative-

    ness of OOP, e.g. SOLID principles • Open Closed • If you add new behavior to an object, you may change the existing behavior that has been declared for the object; that behavior should be built-in, instead
  37. programming paradigms • We invent rules to improve the declarative-

    ness of OOP, e.g. SOLID principles • Liskov Substitution • If using a subclass breaks the program, then the declared behavior of the super class is incorrect
  38. programming paradigms • We invent rules to improve the declarative-

    ness of OOP, e.g. SOLID principles • Interface Separation • Unnecessary methods on bulky interfaces means there are unnecessary declared dependencies between objects
  39. programming paradigms • We invent rules to improve the declarative-

    ness of OOP, e.g. SOLID principles • Dependency Inversion • Decoupling implementation details makes the declared behavior more consistent, easier to manage, easier to validate
  40. programming paradigms • Takeaways • Declarative-ness is a gradient •

    How you view declarative-ness is tied to the kind of abstraction you construct • More declarative code is more valuable; it is simpler to express, easier to understand
  41. programming paradigms fun main() { val x = calc() println(x)

    } fun func(): Int { val a = 1 val b = 2 return a + b (de fi ne (main) (let ((x sum)) (display x))) (de fi ne (sum) (+ 1 2))
  42. programming paradigms fun main() { val x = calc() println(x)

    } fun func(): Int { val a = 1 val b = 2 return a + b (de fi ne (main) (let ((x sum)) (display x))) (de fi ne (sum) (+ 1 2)) Record feature A way to reference data
  43. programming paradigms fun main() { val x = calc() println(x)

    } fun func(): Int { val a = 1 val b = 2 return a + b (de fi ne (main) (let ((x sum)) (display x))) (de fi ne (sum) (+ 1 2)) Closure feature A way to scope “work"
  44. programming paradigms fun main() { var x = calc() println(x)

    } fun func(): Int { val a = 1 val b = 2 return a + b (de fi ne (main) (let ((x sum)) (display x))) (de fi ne (sum) (+ 1 2)) Named state feature State over time, variables.
  45. programming paradigms • Concurrency paradigms • It’d be better if

    we could declare that our code was concurrent, rather than implement the concurrency ourselves
  46. programming paradigms • Concurrency paradigms • It’d be better if

    we could declare that our code was concurrent, rather than implement the concurrency ourselves • Our tools by default are pretty imperative though, we pretend we’re a CPU and manually manage threads
  47. programming paradigms var listeners = emptySet<Listener>() fun add(l: Listener) {

    listeners = listeners + l } fun ping() { listeners.each { it.ping() } }
  48. programming paradigms ConcurrentModificationException var listeners = emptySet<Listener>() fun add(l: Listener)

    { listeners = listeners + l } fun ping() { listeners.each { it.ping() } }
  49. programming paradigms var listeners = emptySet<Listener>() fun add(l: Listener) =

    synchronized(this) { listeners = listeners + l } fun ping() = synchronized(this) { listeners.each { it.ping() } }
  50. programming paradigms • Shared-state concurrent programming • Concurrency like threads,

    shared access to resource, transaction locking • Ex: Java Executors • Impossible to have confidence of the order of execution by the scheduler, so enforce order with single-threaded operation
  51. programming paradigms • Message-passing concurrent programming • Multiple single-threaded components

    guard resources, send/respond to events to/from other components • Ex: Swift Actors • Single-thread is more deterministic, but less concurrent
  52. programming paradigms var listeners = emptySet<Listener>() while (true) { val

    m = messages.poll() when (m) { is Add -> listeners += m.listener is Ping -> listeners.each { it.ping() } } }
  53. programming paradigms • Continuous synchronous programming • Outputs instantly follow

    inputs • Very theoretical, like a math equation for time t
  54. programming paradigms • Continuous synchronous programming • Outputs instantly follow

    inputs • Very theoretical, like a math equation for time t • Also known as functional reactive programming
  55. programming paradigms • Discrete synchronous programming • Synchronous programming as

    in synchronous circuits, outputs follow inputs after a discrete clock “tick”
  56. programming paradigms • Discrete synchronous programming • Synchronous programming as

    in synchronous circuits, outputs follow inputs after a discrete clock “tick” • Also known as reactive programming formally • Ex: RxJava
  57. programming paradigms • RxJava • In an Rx stream, Source

    and Observers coordinate to ensure the same inputs always results in the same outputs • Imagine each input associated with a clock tick, and the input progressing through each operator on each tick
  58. programming paradigms • RxJava declares the guarantees it provides when

    dealing with concurrent events • Within an Rx stream, we can trust that there is a deterministic order of operations
  59. programming paradigms • Continuation programming • Resume execution at a

    later point in time • Ex: Kotlin coroutines, Exceptions, gotos
  60. programming paradigms • Continuation programming • Resume execution at a

    later point in time • Ex: Kotlin coroutines, Exceptions, gotos • Embedding “threads” in the imperative model feels natural, as our brains operate in the imperative world
  61. programming paradigms • Continuation programming • Resume execution at a

    later point in time • Ex: Kotlin coroutines, Exceptions, gotos • Embedding “threads” in the imperative model feels natural, as our brains operate in the imperative world • Async by default is very powerful
  62. programming paradigms • Takeaways • So you’re saying we already

    have all the tools, paradigms, patterns we need to write perfectly deterministic, declarative code?
  63. programming paradigms • Takeaways • So you’re saying we already

    have all the tools, paradigms, patterns we need to write perfectly deterministic, declarative code? • What happens if we misuse these patterns?
  64. programming paradigms val relay = PublishRelay.create<Int>() Observable.from(1, 2, 3) .switchMap

    { x -> relay.accept(x) Observable.just(x) } .subscribe { relay.accept(x) }
  65. val relay = PublishRelay.create<Int>() Observable.from(1, 2, 3) .switchMap { x

    -> relay.accept(x) Observable.just(x) } .subscribe { relay.accept(x) } programming paradigms Single streams are deterministic, multiple streams are harder to reason
  66. programming paradigms class Emitter { fun emitsSomePattern() = Observable.create {

    emitter -> val relay = PublishRelay.create<Int>() val subs = CompositeDisposable() emitter.setDisposable(subs) subs.add( relay.subscribe(emitter::onNext) ) subs.add( Observable.from(1, 2, 3) .switchMap { x -> relay.accept(x) Observable.just(x) } .subscribe { relay.accept(x) } ) } } Non-deterministic in the small, maybe deterministic in the large. Gradient!
  67. programming paradigms If our entire application was a single Rx

    stream, then we could be more confident that its entire behavior could be deterministic. class Emitter { fun emitsSomePattern() = Observable.create { emitter -> val relay = PublishRelay.create<Int>() val subs = CompositeDisposable() emitter.setDisposable(subs) subs.add( relay.subscribe(emitter::onNext) ) subs.add( Observable.from(1, 2, 3) .switchMap { x -> relay.accept(x) Observable.just(x) } .subscribe { relay.accept(x) } ) } }
  68. programming paradigms But it’s hard to write a complex app

    as a single stream… class Emitter { fun emitsSomePattern() = Observable.create { emitter -> val relay = PublishRelay.create<Int>() val subs = CompositeDisposable() emitter.setDisposable(subs) subs.add( relay.subscribe(emitter::onNext) ) subs.add( Observable.from(1, 2, 3) .switchMap { x -> relay.accept(x) Observable.just(x) } .subscribe { relay.accept(x) } ) } }
  69. programming paradigms So how do we reconcile using a declarative,

    deterministic paradigm in a way that reduces its declarative-ness? class Emitter { fun emitsSomePattern() = Observable.create { emitter -> val relay = PublishRelay.create<Int>() val subs = CompositeDisposable() emitter.setDisposable(subs) subs.add( relay.subscribe(emitter::onNext) ) subs.add( Observable.from(1, 2, 3) .switchMap { x -> relay.accept(x) Observable.just(x) } .subscribe { relay.accept(x) } ) } }
  70. strong abstractions • What does it mean to misuse a

    paradigm? • Differentiate between the paradigm, and the language’s implementation of the paradigm
  71. strong abstractions fun main() { var x = calc() println(x)

    } fun func(): Int { val a = 1 val b = 2 return a + b (de fi ne (main) (let ((x sum)) (display x))) (de fi ne (sum) (+ 1 2)) Functional programming
  72. strong abstractions fun main() { var x = calc() println(x)

    } fun func(): Int { val a = 1 val b = 2 return a + b (de fi ne (main) (let ((x sum)) (display x))) (de fi ne (sum) (+ 1 2)) Scheme forbids* mutability
  73. strong abstractions fun main() { var x = calc() println(x)

    } fun func(): Int { val a = 1 val b = 2 return a + b (de fi ne (main) (let ((x 0)) (set! x sum) (display x))) (de fi ne (sum) (+ 1 2)) Scheme discourages mutability in its language design
  74. strong abstractions • Differentiate between the paradigm, and the language’s

    implementation of the paradigm • Some implementations of the paradigm are better than others • Harder to make mistakes • More declarative
  75. fun execute(): Single<Int> { return Single.fromCallable { getValue() } }

    strong abstractions Rx has lots of rules to use safely, to maintain its declarative-ness
  76. fun execute(): Single<Int> { return Single.fromCallable { getValue() } }

    strong abstractions When we get it right, we have a very powerful tool
  77. fun execute(): Flow<Int> = { val x = getValue() return

    fl ow { emit(x) } } strong abstractions Problems not limited to Java
  78. async fun execute(): Int { return getValue() } strong abstractions

    Different pattern prevents making that mistake altogether
  79. async fun execute(): Int { return getValue() } strong abstractions

    Also think about how you cannot start a coroutine without a scope
  80. strong abstractions • A strong paradigm abstraction makes it hard

    to make mistakes that break the rules of the paradigm abstraction
  81. strong abstractions • A strong paradigm abstraction makes it hard

    to make mistakes that break the rules of the paradigm abstraction • Sometimes, we don’t even know what rules we’ve broken!
  82. val relay = PublishRelay.create<Int>() Observable.from(1, 2, 3) .switchMap { x

    -> relay.accept(x) Observable.just(x) } .subscribe { relay.accept(x) } strong abstractions Why are we allowed to even break the rules?
  83. val relay = PublishRelay.create<Int>() Observable.from(1, 2, 3) .switchMap { x

    -> relay.accept(x) Observable.just(x) } .subscribe { relay.accept(x) } strong abstractions Why are we allowed to even break the rules? Java is very flexible.
  84. strong abstractions • SOLID for RxJava, Flow • Dependency inversion

    • All events in a stream should be at the same level of abstraction
  85. strong abstractions • SOLID for RxJava, Flow • Subscribe for

    side-effects • Interactions with non-stream behaviors should be limited to the subscribe handler • Avoid emitting into other streams from the middle of a stream • “Safe” side-effects, like logging, excluded
  86. strong abstractions • SOLID for RxJava, Flow • One stream

    • Consolidating behavior into a single stream maintains the declarative-ness of the flow of events • Return Observables, not Disposables
  87. strong abstractions • SOLID for RxJava, Flow • L… Safe

    bridges • Use Observable.create, Relays, to create safe bridges between the imperative and reactive world
  88. fun events() = Observable.create { emitter -> val l =

    object : Listener { override fun onEvent() = emitter.onNext(OnEvent) } listeners.setListener(l) emitter.setCancellable { listeners.removeListener(l) } } strong abstractions
  89. strong abstractions • Takeaways • Consider the declarative-ness of the

    code when using a new pattern • Learn the rules of the pattern, or create your own rules to maintain the advantages of the pattern
  90. strong abstractions • What are the rules for other programming

    paradigms, like coroutines? • Ex: How bad is emitting into Flows from coroutines you’re also collecting?
  91. strong abstractions • What are the rules for other programming

    paradigms, like coroutines? • Ex: How bad is emitting into Flows from coroutines you’re also collecting? • What are the rules for your own architectures, libraries? How easy is it to break those rules?
  92. strong abstractions • What is the next programming paradigm? •

    We invent new paradigms all the time building architectures, solving problems
  93. strong abstractions • What is the next programming paradigm? •

    We invent new paradigms abstractions all the time building architectures, solving problems
  94. strong abstractions • What is the next programming paradigm? •

    We invent new paradigms abstractions all the time building architectures, solving problems • A paradigm is just the name of an abstraction of how we think about our programs
  95. strong abstractions • As we invent our own abstractions, we

    can think about how to make them stronger, more expressive, more declarative, more deterministic, to solve our needs
  96. strong abstractions • As we invent our own abstractions, we

    can think about how to make them stronger, more expressive, more declarative, more deterministic, to solve our needs • As we use other abstractions, we should think about how to use them safely, to maintain their declarative-ness
  97. further reading • Programming Paradigms for Dummies • Peter Van

    Roy • Build an 8-bit computer from scratch • Ben Eater • Conal Elliot’s blog (creator(?) of FRP) victor lai dblisse