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

Multiplatform Functional Architecture with Oolong

Multiplatform Functional Architecture with Oolong

Software development is becoming increasingly functional, and for good reason. Functional programming principles like abstraction and composition enable safe, scalable, feature-driven applications. Learn how Oolong uses the Model-View-Update architectural pattern to help you write software with confidence and ease.

Michael Pardo

October 08, 2020
Tweet

More Decks by Michael Pardo

Other Decks in Programming

Transcript

  1. None
  2. Multiplatform Functional Architecture

  3. Multiplatform Functional Architecture with Oolong

  4. None
  5. Different codebases solving the same problems.

  6. None
  7. None
  8. None
  9. Multiplatform Functional Architecture

  10. https://kotlinlang.org/docs/reference/multiplatform.html

  11. Multiplatform Functional Architecture

  12. λx.x a └┬─┘ Application Body ┌┴┐ Variable ┌┴┐ └───┬────┘ Abstraction

  13. (λU.(λY.(λvoid.(λ0.(λsucc.(λ+.(λ*.(λ1.(λ2.(λ3.(λ4.(λ5.(λ6.(λ7.(λ8.(λ9.(λ10.(λnum.(λtrue.(λfalse.(λif.(λnot. (λand.(λor.(λmake-pair.(λpair-first.(λpair-second.(λzero?.(λpred.(λ-.(λeq?.(λ/.(λ%.(λnil.(λnil?.(λcons. (λcar.(λcdr.(λdo2.(λdo3.(λdo4.(λfor.(λprint-byte.(λprint-list.(λprint-newline.(λzero-byte.(λitoa.(λfizzmsg. (λbuzzmsg.(λfizzbuzzmsg.(λfizzbuzz.(fizzbuzz (((num 1) 0) 1)) λn.((for n)

    λi.((do2 (((if (zero? ((% i) 3))) λ_.(((if (zero? ((% i) 5))) λ_.(print-list fizzbuzzmsg)) λ_.(print-list fizzmsg))) λ_.(((if (zero? ((% i) 5))) λ_.(print-list buzzmsg)) λ_.(print-list (itoa i))))) (print-newline nil)))) ((cons (((num 0) 7) 0)) ((cons (((num 1) 0) 5)) ((cons (((num 1) 2) 2)) ((cons (((num 1) 2) 2)) ((cons (((num 0) 9) 8)) ((cons (((num 1) 1) 7)) ((cons (((num 1) 2) 2)) ((cons (((num 1) 2) 2)) nil))))))))) ((cons (((num 0) 6) 6)) ((cons (((num 1) 1) 7)) ((cons (((num 1) 2) 2)) ((cons (((num 1) 2) 2)) nil))))) ((cons (((num 0) 7) 0)) ((cons (((num 1) 0) 5)) ((cons (((num 1) 2) 2)) ((cons (((num 1) 2) 2)) nil))))) λn.(((Y λrecurse.λn.λresult.(((if (zero? n)) λ_.(((if (nil? result)) λ_.((cons zero-byte) nil)) λ_.result)) λ_. ((recurse ((/ n) 10)) ((cons ((+ zero-byte) ((% n) 10))) result)))) n) nil)) (((num 0) 4) 8)) λ_.(print- byte (((num 0) 1) 0))) (Y λrecurse.λl.(((if (nil? l)) λ_.void) λ_.((do2 (print-byte (car l))) (recurse (cdr l)))))) PRINT_BYTE) λn.λf.((((Y λrecurse.λremaining.λcurrent.λf.(((if (zero? remaining)) λ_.void) λ_.((do2 (f current)) (((recurse (pred remaining)) (succ current)) f)))) n) 0) f)) λa.do3) λa.do2) λa.λb.b) λl. (pair-second (pair-second l))) λl.(pair-first (pair-second l))) λe.λl.((make-pair true) ((make-pair e) l))) λl.(not (pair-first l))) ((make-pair false) void)) λm.λn.((- m) ((* ((/ m) n)) n))) (Y λ/.λm.λn.(((if ((eq? m) n)) λ_.1) λ_.(((if (zero? ((- m) n))) λ_.0) λ_.((+ 1) ((/ ((- m) n)) n)))))) λm.λn.((and (zero? ((- m) n))) (zero? ((- n) m)))) λm.λn.((n pred) m)) λn.(((λn.λf.λx.(pair-second ((n λp.((make-pair (f (pair-first p))) (pair-first p))) ((make-pair x) x))) n) succ) 0)) λn.((n λ_.false) true)) λp.(p false)) λp.(p true)) λx.λy.λt.((t x) y)) λa.λb.((a true) b)) λa.λb.((a b) false)) λp.λt.λf.((p f) t)) λp.λa.λb.(((p a) b) void)) λt.λf.f) λt.λf.t) λa.λb.λc.((+ ((+ ((* ((* 10) 10)) a)) ((* 10) b))) c)) (succ 9)) (succ 8)) (succ 7)) (succ 6)) (succ 5)) (succ 4)) (succ 3)) (succ 2)) (succ 1)) (succ 0)) λm.λn.λx.(m (n x))) λm.λn.λf.λx.((((m succ) n) f) x)) λn.λf.λx.(f ((n f) x))) λf.λx.x) λx.(U U)) (U λh.λf.(f λx.(((h h) f) x)))) λf.(f f))
  14. Pure functions

  15. Pure functions are deterministic.

  16. Pure functions are free of side-effects.

  17. Functional programming is abstraction and composition.

  18. Multiplatform Functional Architecture

  19. Boundaries

  20. Impure Pure

  21. None
  22. None
  23. None
  24. Functional architecture is abstraction and composition.

  25. Purity

  26. Purity Boundaries

  27. Purity Boundaries Composition

  28. Architecture Patterns are architecture style + context.

  29. Model View Update UI architecture pattern

  30. Oolong MVU for Kotlin Multiplatform https://oolong-kt.org

  31. None
  32. None
  33. None
  34. None
  35. None
  36. None
  37. None
  38. None
  39. None
  40. None
  41. None
  42. None
  43. None
  44. None
  45. https://staltz.com/unidirectional-user-interface-architectures.html#elm

  46. Purity

  47. None
  48. None
  49. None
  50. data class Model( )

  51. data class Model( val count: Int )

  52. data class Model( val count: Int ) sealed class Msg

    { }
  53. data class Model( val count: Int ) sealed class Msg

    { object Increment : Msg() }
  54. data class Model( val count: Int ) sealed class Msg

    { object Increment : Msg() object Decrement : Msg() }
  55. data class Model( val count: Int ) sealed class Msg

    { object Increment : Msg() object Decrement : Msg() } data class Props( )
  56. data class Model( val count: Int ) sealed class Msg

    { object Increment : Msg() object Decrement : Msg() } data class Props( val count: Int, )
  57. data class Model( val count: Int ) sealed class Msg

    { object Increment : Msg() object Decrement : Msg() } data class Props( val count: Int, val increment: (Dispatch<Msg>) -> Unit, )
  58. data class Model( val count: Int ) sealed class Msg

    { object Increment : Msg() object Decrement : Msg() } data class Props( val count: Int, val increment: (Dispatch<Msg>) -> Unit, val decrement: (Dispatch<Msg>) -> Unit, )
  59. typealias Dispatch<Msg>

  60. typealias Dispatch<Msg> = (Msg) -> Unit

  61. None
  62. None
  63. val init: () -> Pair<Model, Effect<Msg>> = { }

  64. val init: () -> Pair<Model, Effect<Msg>> = { Model(count =

    0) }
  65. val init: () -> Pair<Model, Effect<Msg>> = { Model(count =

    0) to none() }
  66. val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = { msg,

    model -> }
  67. val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = { msg,

    model -> when (msg) { } }
  68. val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = { msg,

    model -> when (msg) { Increment -> } }
  69. val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = { msg,

    model -> when (msg) { Increment -> model.copy(count = model.count + 1) } }
  70. val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = { msg,

    model -> when (msg) { Increment -> model.copy(count = model.count + 1) Decrement -> } }
  71. val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = { msg,

    model -> when (msg) { Increment -> model.copy(count = model.count + 1) Decrement -> model.copy(count = model.count - 1) } }
  72. val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = { msg,

    model -> when (msg) { Increment -> model.copy(count = model.count + 1) Decrement -> model.copy(count = model.count - 1) } to none() }
  73. val view: (Model) -> Props = { model -> }

  74. val view: (Model) -> Props = { model -> Props(

    ) }
  75. val view: (Model) -> Props = { model -> Props(

    count = model.count, ) }
  76. val view: (Model) -> Props = { model -> Props(

    count = model.count, increment = { dispatch -> dispatch(Increment) }, ) }
  77. val view: (Model) -> Props = { model -> Props(

    count = model.count, increment = { dispatch -> dispatch(Increment) }, decrement = { dispatch -> dispatch(Decrement) }, ) }
  78. runtime( init, update, view, render )

  79. runtime( init, update, view, render )

  80. runtime( init, update, view, render )

  81. val render: (Props, Dispatch<Msg>) -> Any? = { props, dispatch

    -> }
  82. val render: (Props, Dispatch<Msg>) -> Any? = { props, dispatch

    -> setContent { } }
  83. val render: (Props, Dispatch<Msg>) -> Any? = { props, dispatch

    -> setContent { Counter(props, dispatch) } }
  84. @Composable fun Counter(props: Props, dispatch: Dispatch<Msg>) { }

  85. @Composable fun Counter(props: Props, dispatch: Dispatch<Msg>) { OutlinedButton(onClick = {

    }) { Icon(vectorResource(R.drawable.ic_decrement)) } }
  86. @Composable fun Counter(props: Props, dispatch: Dispatch<Msg>) { OutlinedButton(onClick = {

    props.decrement(dispatch) }) { Icon(vectorResource(R.drawable.ic_decrement)) } }
  87. @Composable fun Counter(props: Props, dispatch: Dispatch<Msg>) { OutlinedButton(onClick = {

    props.decrement(dispatch) }) { Icon(vectorResource(R.drawable.ic_decrement)) } Text("") }
  88. @Composable fun Counter(props: Props, dispatch: Dispatch<Msg>) { OutlinedButton(onClick = {

    props.decrement(dispatch) }) { Icon(vectorResource(R.drawable.ic_decrement)) } Text("${props.count}") }
  89. @Composable fun Counter(props: Props, dispatch: Dispatch<Msg>) { OutlinedButton(onClick = {

    props.decrement(dispatch) }) { Icon(vectorResource(R.drawable.ic_decrement)) } Text("${props.count}") Button(onClick = { }) { Icon(vectorResource(R.drawable.ic_increment)) } }
  90. @Composable fun Counter(props: Props, dispatch: Dispatch<Msg>) { OutlinedButton(onClick = {

    props.decrement(dispatch) }) { Icon(vectorResource(R.drawable.ic_decrement)) } Text("${props.count}") Button(onClick = { props.increment(dispatch) }) { Icon(vectorResource(R.drawable.ic_increment)) } }
  91. Testing

  92. @Test fun `initial Model count should be 0`() { }

  93. @Test fun `initial Model count should be 0`() { val

    expected = Model(count = 0) }
  94. @Test fun `initial Model count should be 0`() { val

    expected = Model(count = 0) val (actual, _) = init() }
  95. @Test fun `initial Model count should be 0`() { val

    expected = Model(count = 0) val (actual, _) = init() assertEquals(expected, actual) }
  96. @Test fun `Increment msg should increment Model count`() { }

  97. @Test fun `Increment msg should increment Model count`() { val

    msg = Msg.Increment val model = Model(count = 0) val expected = Model(count = 1) }
  98. @Test fun `Increment msg should increment Model count`() { val

    msg = Msg.Increment val model = Model(count = 0) val expected = Model(count = 1) val (actual, _) = update(msg, model) }
  99. @Test fun `Increment msg should increment Model count`() { val

    msg = Msg.Increment val model = Model(count = 0) val expected = Model(count = 1) val (actual, _) = update(msg, model) assertEquals(expected, actual) }
  100. @Test fun `Decrement msg should increment Model count`() { }

  101. @Test fun `Decrement msg should increment Model count`() { val

    msg = Msg.Decrement val model = Model(count = 0) val expected = Model(count = -1) }
  102. @Test fun `Decrement msg should increment Model count`() { val

    msg = Msg.Decrement val model = Model(count = 0) val expected = Model(count = -1) val (actual, _) = update(msg, model) }
  103. @Test fun `Decrement msg should increment Model count`() { val

    msg = Msg.Decrement val model = Model(count = 0) val expected = Model(count = -1) val (actual, _) = update(msg, model) assertEquals(expected, actual) }
  104. @Test fun `Props count should be equal to Model count`()

    { }
  105. @Test fun `Props count should be equal to Model count`()

    { val model = Model(count = 0) val expected = model.count }
  106. @Test fun `Props count should be equal to Model count`()

    { val model = Model(count = 0) val expected = model.count val props = view(model) val actual = props.count }
  107. @Test fun `Props count should be equal to Model count`()

    { val model = Model(count = 0) val expected = model.count val props = view(model) val actual = props.count assertEquals(expected, actual) }
  108. @Test fun `Props increment should dispatch Increment msg`() { }

  109. @Test fun `Props increment should dispatch Increment msg`() { val

    model = Model(count = 0) val props = view(model) }
  110. @Test fun `Props increment should dispatch Increment msg`() { val

    model = Model(count = 0) val props = view(model) props.increment { msg -> } }
  111. @Test fun `Props increment should dispatch Increment msg`() { val

    model = Model(count = 0) val props = view(model) props.increment { msg -> assertEquals(msg, Msg.Increment) } }
  112. @Test fun `Props decrement should dispatch Decrement msg`() { }

  113. @Test fun `Props decrement should dispatch Decrement msg`() { val

    model = Model(count = 0) val props = view(model) }
  114. @Test fun `Props decrement should dispatch Decrement msg`() { val

    model = Model(count = 0) val props = view(model) props.decrement { msg -> } }
  115. @Test fun `Props decrement should dispatch Decrement msg`() { val

    model = Model(count = 0) val props = view(model) props.decrement { msg -> assertEquals(msg, Msg.Decrement) } }
  116. Boundaries

  117. typealias GetCount

  118. typealias GetCount = () -> Int

  119. typealias GetCount = () -> Int typealias PutCount

  120. typealias GetCount = () -> Int typealias PutCount = (Int)

    -> Unit
  121. class CounterService(context: Context) { }

  122. class CounterService(context: Context) { private val prefs = context.getSharedPreferences("counter", MODE_PRIVATE)

    }
  123. class CounterService(context: Context) { private val prefs = context.getSharedPreferences("counter", MODE_PRIVATE)

    val getCount: GetCount = { } }
  124. class CounterService(context: Context) { private val prefs = context.getSharedPreferences("counter", MODE_PRIVATE)

    val getCount: GetCount = { prefs.getInt("count", 0) } }
  125. class CounterService(context: Context) { private val prefs = context.getSharedPreferences("counter", MODE_PRIVATE)

    val getCount: GetCount = { prefs.getInt("count", 0) } val putCount: PutCount = { count -> } }
  126. class CounterService(context: Context) { private val prefs = context.getSharedPreferences("counter", MODE_PRIVATE)

    val getCount: GetCount = { prefs.getInt("count", 0) } val putCount: PutCount = { count -> prefs.edit().putInt("count", count).apply() } }
  127. { } { count -> } // typealias GetCount =

    () -> Int // typealias PutCount = (Int) -> Unit val getCount: GetCount = val putCount: PutCount =
  128. Side Effects

  129. Managed Effects

  130. None
  131. None
  132. None
  133. typealias Effect<Msg>

  134. typealias Effect<Msg> = (Dispatch<Msg>) -> Any?

  135. typealias Effect<Msg> = suspend (Dispatch<Msg>) -> Any?

  136. typealias Effect<Msg> = suspend CoroutineScope.(Dispatch<Msg>) -> Any?

  137. private val getCountEffect: (GetCount) -> Effect<Msg> = { getCount ->

    }.
  138. private val getCountEffect: (GetCount) -> Effect<Msg> = { getCount ->

    effect { dispatch -> } }.
  139. private val getCountEffect: (GetCount) -> Effect<Msg> = { getCount ->

    effect { dispatch -> val count = getCount() } }.
  140. private val getCountEffect: (GetCount) -> Effect<Msg> = { getCount ->

    effect { dispatch -> val count = getCount() dispatch(Msg.SetCount(count)) } }.
  141. private val putCountEffect: (PutCount) -> (Int) -> Effect<Msg> = {

    putCount -> }.
  142. private val putCountEffect: (PutCount) -> (Int) -> Effect<Msg> = {.putCount

    -> { count -> }, }.
  143. private val putCountEffect: (PutCount) -> (Int) -> Effect<Msg> = {.putCount

    -> { count -> effect { } }, }.
  144. private val putCountEffect: (PutCount) -> (Int) -> Effect<Msg> = {.putCount

    -> { count -> effect { putCount(count) } }, }.
  145. val init: () -> Pair<Model, Effect<Msg>> = { Model(count =

    0) to none() }
  146. val init: (GetCount) -> () -> Pair<Model, Effect<Msg>> = {.getCount

    -> { Model(count = 0) to none() } }.
  147. val init: (GetCount) -> () -> Pair<Model, Effect<Msg>> = {.getCount

    -> { Model(count = 0) to none() } }
  148. val init: (GetCount) -> () -> Pair<Model, Effect<Msg>> = {.getCount

    -> { Model(count = 0) to getCountEffect(getCount) } }
  149. sealed class Msg { object Increment : Msg() object Decrement

    : Msg() }
  150. sealed class Msg { class SetCount(val count: Int) : Msg()

    object Increment : Msg() object Decrement : Msg() }
  151. val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = { msg,

    model -> when (msg) { Increment -> model.copy(count = model.count + 1) Decrement -> model.copy(count = model.count - 1) } to none() }
  152. val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = { msg,

    model -> when (msg) { is SetCount -> Increment -> model.copy(count = model.count + 1) Decrement -> model.copy(count = model.count - 1) } to none() }
  153. val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = { msg,

    model -> when (msg) { is SetCount -> model.copy(count = msg.count). Increment -> model.copy(count = model.count + 1). Decrement -> model.copy(count = model.count - 1). } to none() }
  154. val update: (PutCount) -> (Msg, Model) -> Pair<Model, Effect<Msg>> =

    { putCount -> { msg, model -> when (msg) { is SetCount -> model.copy(count = msg.count) Increment -> model.copy(count = model.count + 1) Decrement -> model.copy(count = model.count - 1) } to none() } }
  155. val update: (PutCount) -> (Msg, Model) -> Pair<Model, Effect<Msg>> =

    { putCount -> val putCountEffect = putCountEffect(putCount) { msg, model -> when (msg) { is SetCount -> model.copy(count = msg.count) Increment -> model.copy(count = model.count + 1) Decrement -> model.copy(count = model.count - 1) } to none() } }
  156. val update: (PutCount) -> (Msg, Model) -> Pair<Model, Effect<Msg>> =

    { putCount -> val putCountEffect = putCountEffect(putCount) { msg, model -> when (msg) { is SetCount -> model.copy(count = msg.count) Increment -> model.copy(count = model.count + 1) Decrement -> model.copy(count = model.count - 1) } to none() } }
  157. val update: (PutCount) -> (Msg, Model) -> Pair<Model, Effect<Msg>> =

    { putCount -> val putCountEffect = putCountEffect(putCount) { msg, model -> when (msg) { is SetCount -> model.copy(count = msg.count) Increment -> model.copy(count = model.count + 1) Decrement -> model.copy(count = model.count - 1) } to putCountEffect(model.count) } }
  158. runtime( init, update, view, render )

  159. runtime( init, update, view, render )

  160. runtime( init(counterService.getCount), update, view, render )

  161. runtime( init(counterService.getCount), update, view, render )

  162. runtime( init(counterService.getCount), update(counterService.putCount), view, render )

  163. @Test fun `initial Effect should dispatch SetCount`() { }

  164. @Test fun `initial Effect should dispatch SetCount`() { val getCount:

    GetCount = { 0 } val expected = Msg.SetCount(getCount()) val (_, effect) = init() }
  165. @Test fun `initial Effect should dispatch SetCount`() { val getCount:

    GetCount = { 0 } val expected = Msg.SetCount(getCount()) val (_, effect) = init() runBlocking { effect { actual -> } } }
  166. @Test fun `initial Effect should dispatch SetCount`() { val getCount:

    GetCount = { 0 } val expected = Msg.SetCount(getCount()) val (_, effect) = init() runBlocking { effect { actual -> assertEquals(expected, actual) } } }
  167. @Test fun `update effect should put count`() { }

  168. @Test fun `update effect should put count`() { val msg

    = Msg.SetCount(count = 42) val model = Model(count = 0) val putCount: PutCount = { count -> } }
  169. @Test fun `update effect should put count`() { val msg

    = Msg.SetCount(count = 42) val model = Model(count = 0) val putCount: PutCount = { count -> } val (_, effect) = update(putCount)(msg, model) runBlocking { effect { } } }
  170. @Test fun `update effect should put count`() { val msg

    = Msg.SetCount(count = 42) val model = Model(count = 0) val putCount: PutCount = { count -> assertEquals(count, msg.count) } val (_, effect) = update(putCount)(msg, model) runBlocking { effect { } } }
  171. Composition

  172. None
  173. object Child { }

  174. object Child { class Model }

  175. object Child { class Model class Msg }

  176. object Child { class Model class Msg class Props }

  177. object Child { class Model class Msg class Props val

    init: () -> Pair<Model, Effect<Msg>> = ... }
  178. object Child { class Model class Msg class Props val

    init: () -> Pair<Model, Effect<Msg>> = ... val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = ... }
  179. object Child { class Model class Msg class Props val

    init: () -> Pair<Model, Effect<Msg>> = ... val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = ... val view: (Model) -> Props = ... }
  180. object Child { class Model class Msg class Props val

    init: () -> Pair<Model, Effect<Msg>> = ... val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = ... val view: (Model) -> Props = ... }
  181. object Parent { class.Model class Msg class Props val init:

    () -> Pair<Model, Effect<Msg>> = ... val update: (Msg, Model).-> Pair<Model, Effect<Msg>> = ... val view: (Model).-> Props = ... }
  182. class Model( val child: Child.Model )

  183. sealed class Msg { class ChildMsg( val child: Child.Msg )

    : Msg() }
  184. class Props( val child: Child.Props )

  185. val init: () -> Pair<Model, Effect<Msg>> = { // Child.init()

    }
  186. val update: (Msg, Model) -> Pair<Model, Effect<Msg>> = { msg,

    model -> // Child.update() }
  187. val view: (Model) -> Props = { model -> //

    Child.view() }
  188. data class Model( )

  189. data class Model( val counters: Map<Id, Counter.Model> )

  190. sealed class Msg { }

  191. sealed class Msg { class SetCounts(val counts: Map<Id, Int>) :

    Msg() object AddCounter : Msg() class RemoveCounter(val id: Id) : Msg() class CounterMsg( val id: Id, val msg: Counter.Msg ) : Msg() }
  192. class Props( )

  193. class Props( val counters: List<CounterRow>, val addCounter: (Dispatch<Msg>) -> Unit,

    )
  194. class Props( val counters: List<CounterRow>, val addCounter: (Dispatch<Msg>) -> Unit,

    ) { class CounterRow( ) }
  195. class Props( val counters: List<CounterRow>, val addCounter: (Dispatch<Msg>) -> Unit,

    ) { class CounterRow( val props: Counter.Props, val removeCounter: (Dispatch<Msg>) -> Unit, ) }
  196. github.com/pardom/mfa-with-oolong

  197. What Have We Learned?

  198. Purity

  199. Purity Boundaries

  200. Purity Boundaries Composition

  201. Architecture Patterns are architecture style + context.

  202. Model View Update UI architecture pattern

  203. None
  204. Multiplatform Functional Architecture with Oolong • oolong-kt.org • guide.elm-lang.org •

    github.com/pardom/mfa-with-oolong