TEA Bag Usage Sample - Xlopec/Tea-bag GitHub Wiki

We'll start from something simple, for example, our sample from README. Let's say we want to implement application that adds two numbers ¯_(ツ)_/¯, displays the result and sends it to some analytic service.

Initial Implementation

Okay, with all these inputs in place now we're ready to implement our application. Let's begin implementation from domain. After hours of trials we've got the following function that calculates the sum of two numbers:

fun add(
    delta: Int,
    counter: Int,
): Int = counter + delta

Cool, but what about initial inputs? We must have initial counter value to start with. Our domain expert says that it's going to be 0 for now. Also, he says that both analytics and output data can be written to console. Let's do it!

// provides initial counter value
fun initializer(): Int = 0

// tracker implementation
fun track(
    delta: Int,
    counter: Int,
) {
    println("Track: delta $delta, counter $counter")
}

// display implementation
fun display(
    delta: Int,
    counter: Int,
) {
    println("Display: delta $delta, counter $counter")
}

Putting it all together we'll get:

fun main() {
    val delta = 1
    var counter = add(delta = delta, counter = initializer())
    
    track(delta, counter)
    display(delta, counter)
    counter = add(delta = delta, counter = initializer())
    // ... more calculations
    track(delta, counter)
    display(delta, counter)
}

We did it! Everything works and looks simple, however, this code doesn't scale well. Problems start arising when we start working with mutable state, and things are getting worse when concurrency comes into play.

TEA Implementation

Let's say the domain expert came to us and said that the initial counter value is stored on a remote DB and we should make our initializer return values from that remote database. Also, we shouldn't forget any updates made to counter must be visible across the app and reflected on UI.

We can try tackling all the aforementioned problems using TEA. We'll implement new requirements a bit later. Let's first adjust our code to use the TEA-Bag library.

Taking Initializer's signature into account it becomes:

public typealias Initializer<S, C> = suspend () -> Initial<S, C>

/**Async initializer, provides initial state*/
suspend fun initializer(): Initial<Int, Int> = Initial(0)

The same goes for other functions. After refactoring they look like the following:

public typealias Updater<M, S, C> = (message: M, state: S) -> Update<S, C>

public typealias Update<S, C> = Pair<S, Set<C>>

/**App logic, for now it just adds delta to count and returns this as result*/
fun add(
    delta: Int,
    counter: Int,
): Update<Int, Int> = (counter + delta) to setOf(delta)

/**Some UI, e.g. console*/
suspend fun display(
    snapshot: Snapshot<*, *, *>,
) {
    println("Display: $snapshot")
}

/**Some tracker*/
fun track(
    event: Snapshot<Int, Int, Int>,
    ctx: ResolveCtx<Int>,
) {
    ctx sideEffect { println("Track: \"$event\"") }
}

Our main function becomes:

fun main() = runBlocking {
    // Somewhere at the application level
    val component = Component(
        initializer = ::initializer,
        resolver = ::track,
        updater = ::add,
        scope = this,
    )
    // UI = component([message1, message2, ..., message N])
    component(+1, +2, -3).collect(::display)
}

Which produces:

Display: Initial(currentState=0, commands=[])
Track: "Regular(currentState=1, commands=[1], previousState=0, message=1)"
Track: "Initial(currentState=0, commands=[])"
Track: "Regular(currentState=3, commands=[2], previousState=1, message=2)"
Track: "Regular(currentState=0, commands=[-3], previousState=3, message=-3)"
Display: Regular(currentState=1, commands=[1], previousState=0, message=1)
Display: Regular(currentState=3, commands=[2], previousState=1, message=2)
Display: Regular(currentState=0, commands=[-3], previousState=3, message=-3)

That's it, we implemented our application using the TEA-Bag library! We'll see later how this approach scales

⚠️ **GitHub.com Fallback** ⚠️