class: center, middle, title-slide count: false # **Better `Schedule`s** ## using compositional design patterns
.less-line-height[ Alejandro Serrano @ Kotlin Meetup .grey[🐦 @trupill - 🏠 serranofp.com - 👨💻 JetBrains] ] --- # 🥅 Goals of this talk > How we reworked the implementation of `Schedule` in Arrow Introduction to **compositional** design patterns - Functor and Applicative - (Free) monad -- _Joint work with Simon Vergauwen (`@vergauwen_simon`) and Francisco Díaz_ --- # 🏹 Arrow - `arrow-kt.io` Set of libraries for every Kotlin developer
inspired by functional programming * _Core_: typed errors, non-empty collections * _Fx_: high-level concurrency, resources * _Resilience_: retries, circuit breakers * _Optics_: immutable data transformation * _Atomic_ + _STM_: transactional operations All of them Multiplatform-ready --- # ⏲️ Schedule Description of a policy for executing jobs and (potential) delays between them - To _retry_ until one succeeds - To _repeat_ the same job multiple times Simplest pattern for _resilience_, just try again --- # ⏲️ Schedule examples .code60[ ```kotlin // retry a maximum of 10 times Schedule.recurs
(10) // retry with exponential back-off Schedule.exponential
(250.milliseconds) // 1. exponential back-off for 60 seconds // 2. at most 100 retries spaces by 60 seconds // with some random noise (jitter) Schedule.exponential
(10.milliseconds) .doWhile { _, duration -> duration < 60.seconds } .andThen(spaced
(60.seconds) and recurs(100)) .jittered() ``` ] --- # ⏲️ Schedule examples Using a `Schedule` is pretty easy .code70[ ```kotlin Schedule.recurs(10).repeat { downloadFile(url) } ``` ] - `retry` repeats if an exception is thrown - More variations for custom error conditions -- > You can read about that in the docs
> here we want to talk about the **internals** --- # 🛡️ `Schedule
` .margin-top[ - `Input` specifies the data you base your decision upon - `Output` specifies the result of the action ] ```kotlin recurs(10) : Schedule
// iteration count ↲ doWhile(f) : Schedule
// same value back ↲ ``` --- # 😵💫 Complex implementation Internal `State` type parameter
.little-margin-top[ Unchecked casts ]
--- # 😵💫 Complex implementation Internal `State` type parameter Unchecked casts ...
> We looked for alternatives for a long time --- class: center, middle, title-slide count: false # Compositional design patterns --- # 🌱 Functor This is a pattern over _generic_ types ```kotlin fun
Whatever
.map( transform: (A) -> B ): Whatever
``` - Transforms each element using `transform` - Keeps the shape of the structure
*
-- .font60[
*
There is a formal definition of "keeping the shape": `e.map { it } == e` ] --- # 🌱 Examples of functor .margin-top[ - Iterable types: `List`, `Set`, ... ] -- - Map-key values are functors for a fixed key .code70[ ```kotlin fun
Map
.mapValues( transform: (A) -> B ): Map
``` ] --- # 🌱 Examples of functor .margin-top[ - Iterable types: `List`, `Set`, ... - Map-key values are functors for a fixed key ] - Non-sequential containers .code60[ ```kotlin fun
BinaryTree
.map( transform: (A) -> B ): BinaryTree
= when (this) { is Leaf -> Leaf(transform(value)) is Node -> Node(left.map(transform), right.map(transform)) } ``` ] --- # 🌱 Examples of functor .margin-top[ - Iterable types: `List`, `Set`, ... - Map-key values are functors for a fixed key - Non-sequential containers ] - Nullable types .code70[ ```kotlin fun
A?.map(transform: (A) -> B): B? = this?.let(transform) ``` ] --- # 🪴 Functors are compositional We can write `map` for `F
>`
from the `map`s of each of them --- # 🪴 Functors are compositional We can write `map` for `Map
`
from the `map`s of `Map
` and `A?` .code60[ ```kotlin fun
Map
.mapValues(...): Map
fun
C?.map(...): D? = this?.let(transform) fun
Map
.map( transform: (U) -> V ): Map
= this.mapValues { it?.let(transform) } ``` ] --- # 🪴 Functors are compositional We can write `map` for `F
>`
from the `map`s of each of them ```kotlin fun
F
.mapF(...): F
fun
G
.mapG(...): G
fun
F
>.map( transform: (U) -> V ): F
> = this.mapF { it.mapG(transform) } ``` --- # 🪴 Functors are compositional We can write `map` for `F
>`
from the `map`s of each of them 💡 _We can derive mappers for a complex type_
_from the mappers of its components_ 📚 Having a large library of "basic" mappers
becomes quite useful --- # 🧮 Functions are functors .code60[ ```kotlin fun
Function1
.map( transform: (A) -> B ): Function1
``` ] -- .code60[ ```kotlin fun
((E) -> A).map( transform: (A) -> B ): (E) -> B ``` ] --- # 🧮 Functions are functors .code60[ ```kotlin fun
Function1
.map( transform: (A) -> B ): Function1
``` ] .code60[ ```kotlin fun
((E) -> A).map( transform: (A) -> B ): (E) -> B = { e: E -> val a = this(e) val b = transform(a) b } ``` ] --- # 🧮 Functions are functors .code60[ ```kotlin fun
Function1
.map( transform: (A) -> B ): Function1
``` ] .code60[ ```kotlin fun
((E) -> A).map( transform: (A) -> B ): (E) -> B = { e: E -> transform(this(e)) } ``` ] Function composition! 🤗 --- # 🪷 Applicative Similar to functor, but for transformations
with any number of arguments
*
```kotlin fun
zip( a: Whatever
, b: Whatever
, ..., transform: (A, B, ...) -> Z ): Whatever
``` .font60[
*
Actually you only need the binary case, and you can get the rest by repetition ] --- # 🪷 Examples of applicative .margin-top[ - `Flow` follows this pattern _twice_ ] ```kotlin fun
Flow
.zip( other: Flow
, transform: suspend (T1, T2) -> R ): Flow
fun
Flow
.combine( flow: Flow
, transform: suspend (a: T1, b: T2) -> R ): Flow
``` --- # 🪷 Examples of applicative .margin-top[ - `Flow` follows this pattern _twice_ - Lists and sets - Nullable types - Functions with a given input type ] Not much time to dive into them ⏳ > Applicatives are also compositional --- # 💡 **The** idea `map` and `combine` are the basic API of `Schedule` > Let's define `Schedule` as a composition of basic blocks > so we can derive most implementations auto-magically > ## 🧚 🧚 🧚 🧚 🧚 🧚 🧚 🧚 🧚 🧚 🧚 🧚 --- # 🖼️ `Schedule` in pictures
--- # 📜 `Schedule` in code ```kotlin typealias Step
= (Input) -> Decision
``` ```kotlin sealed interface Decision
{ data class Done(val value: Output): ... data class Continue( val value: Output, val delay: Duration, val step: Step
): Decision
} ``` --- # 📜 `Schedule` in code ```kotlin typealias Step
= (Input) -> Decision
``` 🧚 `Step` is a functor if `Decision` is! -- ## Is `Decision` a functor? 🧚 There is a _recipe_ to build them if: - We have a sealed hierarchy of data classes - Generic `A` appears alone or in a functor --- # 🍳 Recipe for functor .code60[ ```kotlin fun
BinaryTree
.map(transform: (A) -> B) : BinaryTree
= when (this) { is Leaf -> Leaf(transform(value)) is Node -> Node(left.map(transform), right.map(transform)) } ``` ```kotlin fun
Decision
.map(transform: (A) -> B) : Decision
= when (this) { is Done -> Done(transform(value)) is Continue -> Continue( transform(value), // apply on 'A's delay, // leave alone step.map(transform) // map on functors ) } ``` ] --- # 🍳 Recipe for functor In Haskell the recipe is built into the compiler .code70[ ```haskell data BinaryTree a = Leaf { value :: a } | Node { left, right :: BinaryTree a} deriving (Functor) ``` ] --- # 🧱 Simplified `Schedule` `retry` is a loop with matching inside
--- # 🧞 Composing applicatives `Schedule` has two instances of the pattern .code60[ ```kotlin // delay only if both delay fun
fun Schedule
( other: Schedule
, transform: (A, B) -> C ): Schedule
// delay only if any of them delay fun
fun Schedule
( other: Schedule
, transform: (A, B) -> C ): Schedule
``` ] Even one more axis for _combining_ delays --- class: center, middle, title-slide count: false # Monadic adventures --- # 🗓️ Composing `Schedule`s Execute other `Schedule` one the first is done .code70[ ```kotlin fun
Step
.andThen( other: (A) -> Step
): Step
``` ] -- .code70[ ```kotlin fun
((I) -> Decision
).andThen( other: (A) -> Step
): (I) -> Decision
``` ] --- # 🗓️ Composing `Schedule`s Execute other `Schedule` one the first is done .code70[ ```kotlin fun
((I) -> Decision
).andThen( other: (A) -> Step
): (I) -> Decision
= { i: Input -> this(i).andThen(other) } fun
Decision
.andThen( other: (A) -> Step
): Decision
= TODO() ``` ] --- # 🌳 Monad Yet another pattern over _generic_ types ```kotlin fun
Whatever
.flatMap( transform: (A) -> Whatever
): Whatever
``` The difference with functor is that `transform` generates another `Whatever` --- # 🌳 Examples of monad .margin-top[ - Lists (and most iterables) .code70[ ```kotlin fun
List
.flatMap( transform: (A) -> List
): List
``` ] - Nullable types .code70[ ```kotlin fun
A?.flatMap( transform: (A) -> B? ): B? = this?.let(transform) ``` ] ] --- # 🌳 Monads ## Monads are **not compositional** -- But there is an interesting subset
called _free monads_ - Exactly one "success" or "done" case
with one field of generic type - The rest of the cases mention `Whatever
`
but never the generic type alone --- # 🐳 Free monads .margin-top[ - `Either` from Arrow ] ```kotlin sealed interface Either
{ data class Right
(val value: A): ... data class Left
(val error: E): ... } ``` - `Option` and `Result` - Nullable types --- # 🍳 Recipe for free monad .code70[ ```kotlin fun
BinaryTree
.flatMap( transform: (A) -> BinaryTree
): BinaryTree
= when (this) { is Leaf -> transform(value) is Node -> Node( left.map(transform), right.map(transform) ) } ``` ] -- Our `Decision` is quite similar to this! --- # 🎭 Conclusion 1️⃣ _Algebraic_ encoding ⇒ simpler impl. - Describe as "choice of simple records" -- 2️⃣ Use _compositional_ patterns in your API - Powerful primitives to build others - Potentially free implementations --- # 📖 Time for some ads... ## `serranofp.com` > _Books_
--- class: center, middle, title-slide # 🙋 Questions and comments --- class: center, middle, title-slide # 🤩 It's been a pleasure