class: center, middle, title-slide count: false # Better APIs with
Compositional Design Patterns
.less-line-height[ Alejandro Serrano @ Kotlin Dev Day 2025 .grey[.smaller[๐จโ๐ป Kotlin Language Evolution @ JetBrains
๐ฆ @trupill โ ๐ serranofp.com]] ] --- # ๐ฅ Goals of this talk > How we designed the APIs of `Collector` and `Schedule` in Arrow Introduction to **compositional** design patterns - Functor and Applicative - (Free) monad --- # ๐น Arrow - `arrow-kt.io` Set of libraries for every Kotlin developer
inspired by functional programming * _Core_: typed errors, non-empty collections * _Collectors_: aggregation with single traversal * _Fx_: high-level concurrency, resources * _Resilience_: retries, circuit breakers * _Optics_: immutable data transformation * _STM_: transactional operations All of them Multiplatform-ready --- # ๐น Arrow - `arrow-kt.io` Set of libraries for every Kotlin developer
inspired by functional programming * _Core_: ... * _Collectors_: **aggregation with single traversal** * _Fx_: ... * _Resilience_: **retries**, ... * _Optics_: ... * _STM_: ... --- class: center, middle, title-slide count: false # Introducing the characters --- # ๐งบ Collector Aggregate several computations into a single pass over a sequence -- ```kotlin val average = list.sum() / list.size ``` - The `list` is iterated twice - This may not be possible for sequences --- # ๐งบ Collector Aggregate several computations into a single pass over a sequence .code70[ ```kotlin val averageCollector = zip( Collectors.sum, Collectors.length ) { x, y -> x / y } val average = list.collect(averageCollector) ``` ] - Everything is computed in a single pass --- # โฒ๏ธ 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<`A`>.map( transform: (A) -> B ): Whatever<`B`> ``` - 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<`A`>.map( transform: (A) -> B ): BinaryTree<`B`> = 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 --- class: center, middle, title-slide count: false # Magic! --- # ๐ก **The** idea `map` and `combine` as the basic API > Let's define `Collector` and `Schedule` as a composition of basic blocks > so we can derive most implementations > auto-magically ๐ง ๐ง ๐ง ๐ง ๐ง ๐ง ๐ง ๐ง ๐ง --- # ๐งบ Collector First idea: encapsulate the arguments to `fold` .code60[ ```kotlin interface Collector
{ public fun supply(): Result public fun accumulate(current: Result, value: Element) } ``` ] -- ## Is `Collector` a functor? Unfortunately **not** (try at home!) --- # ๐งบ Collector Second idea: separate **accumulator** from result * The accumulator type may not agree with element or results .code60[ ```kotlin typealias Collector
= CollectorI<*, E, R> interface CollectorI
{ public fun supply(): Acc public fun accumulate(current: Acc, value: Element) public fun finish(current: Acc): Result } ``` ] -- Implementations use **local mutability** --- # ๐งบ Collector ## Is `CollectorI` a functor? -- Yes, we just need to "fix" the result .code60[ ```kotlin public fun
Collector
.map( transform: suspend (Result) -> S, ): Collector
= of( supply = this::supply, accumulate = this::accumulate, finish = { current -> `transform(this.finish(current))` }, ) ``` ] --- # ๐งบ Collector ## Is `CollectorI` an applicative? .code70[ ```kotlin fun
zip( collector1: CollectorI
, collector2: CollectorI
, combine: (R, S) -> V ) : CollectorI<`_`, E, V> ``` ] What do we need to have as accumulator? --- # ๐งบ Collector ## Is `CollectorI` an applicative? .code70[ ```kotlin fun
zip( collector1: CollectorI
, collector2: CollectorI
, combine: (R, S) -> V ) : CollectorI<`Pair
`, E, V> ``` ] - Supply and accumulate in parallel - At the end, apply `combine` to the results --- # ๐ง Type arguments Add more type parameters to clarify intermediate states * Implementation becomes "the single one" > Veel parameters maken werk licht -- Actually, the complete advice is separating contra- and co-variant appearances of the same type parameter --- # ๐ผ๏ธ `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 Applying the recipe can be automated > Zo vader, zo zoon -- 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! --- class: center, middle, title-slide count: false # More friends --- # Until now .margin-top[ - Functors - Applicative - Free monads ] ## Are there more? --- # Are there more? .margin-top[ - Contravariant functors: change "inputs" ] .code70[ ```kotlin fun
CollectorI
.contraMap( transform: `(F) -> E` // opposite of 'map' ): CollectorI
``` ] -- * Pointed functors: returns "constants" * Selective functors: limited choice * Alternatives: aggregation or disjunction * Categories: supports composition * ... --- # ๐ญ Conclusion Use _compositional_ patterns in your API - Powerful primitives to build others - Potentially free implementations Developers _recognize_ these patterns -- ๐ _Typeclassopedia_, by Brent Yorgey --- # ๐ 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