Hey! I made a small KMP library for parallel orche...
# coroutines
d
Hey! I made a small KMP library for parallel orchestration using the Applicative pattern. the compiler enforces your dependency graph:
Copy code
// Three primitives — that's the whole model:
//   .ap { }         → parallel ("and at the same time...")
//   .followedBy { } → barrier  ("then, once that's done...")
//   .flatMap { }    → barrier that passes data forward

val checkout = Async {
    lift7(::CheckoutResult)
        .ap { fetchUser(userId) }          // ┐
        .ap { fetchCart(userId) }           // ├─ phase 1: all parallel
        .ap { fetchInventory(userId) }     // ┘
        .followedBy { validateStock() }    // ── barrier: waits for phase 1
        .ap { calcShipping() }             // ┐ phase 2: parallel
        .ap { calcTax() }                  // ┘
        .followedBy { reservePayment() }   // ── barrier
        .ap { sendConfirmation() }         // phase 3
}
// Swap two .ap lines → compiler error.
// 7 service calls, 3 phases, one flat chain.

// When phase 2 NEEDS phase 1 results → flatMap:
val dashboard = Async {
    lift3(::UserContext)
        .ap { fetchProfile(id) }       // ┐ phase 1
        .ap { fetchPrefs(id) }         // │
        .ap { fetchLoyalty(id) }       // ┘
    .flatMap { ctx ->                  // barrier + passes ctx forward
        lift2(::Dashboard)
            .ap { recommendations(ctx.profile) }  // ┐ phase 2
            .ap { promotions(ctx.loyalty) }        // ┘
    }
}
Available on Maven Central:
io.github.damian-rafael-lattenero:kap:1.0.0
https://github.com/damian-rafael-lattenero/coroutines-applicatives Feedback welcome!
c
The library contains a lot more than just that. Is all of that really necessary?
d
Great question! The core is really just three primitives:
.ap
(parallel),
.followedBy
(barrier), and
.flatMap
(barrier that passes data). That's the entire mental model. The rest Schedule, Resource, CircuitBreaker, settled, raceQuorum are opt-in utilities that naturally come up when you use the core in production. You need retries, timeouts, resource cleanup, partial failure tolerance. They're all independent you can use just
lift + ap + followedBy
and ignore everything else. I actually just rewrote the README to make this clearer, with a Before/After comparison for every feature (raw coroutines vs Arrow vs this library): https://github.com/damian-rafael-lattenero/coroutines-applicatives Would love to hear what you think after a fresh look!
c
Do all of these need to be part of the library or can you reuse the ones from Arrow? Having duplicate Either/CircuitBreaker/etc in a project is not very convenient
d
Zero-dep design is intentional th core only needs
kotlinx-coroutines-core
. Most Kotlin projects don't use Arrow, so I didn't want to gate the library behind that. The builtin Either Nel Schedule and Resource are minimal and purpose built for orchestration. For projects that already use Arrow, theres an
arrow-interop
module that bridges the types so no duplication in practice. CircuitBreaker lives in core because it chains directly with Computation (
.withCircuitBreaker(b).retry(s).recover { ... }
). That composability is the point.
c
The problem is; • If I'm a project that doesn't want to use Arrow: if I use your project, I get a ton of lightweight Arrow-like classes that I don't want • If I'm a project that wants to use Arrow: if I use your project, I get a bunch of half-Arrow things duplicated, which pollute auto-complete and such
IMO the best solution is to only keep the Applicative DSL in the 'core', and then have an arrow-compat module which uses the Either/CircuitBreaker/etc directly from Arrow
d
yeah, that’s actually a fair point tbh… I can see both sides way more clearly now. The core DSL (lift, ap, followedBy, flatMap) really should stand on its own, w/o pulling extra stuff you didn’t even ask for. imo the right move is splitting it into modules: like
kap-core
w/ just the Applicative DSL + Computation, super lean… and then
kap-resilience
as opt-in for Schedule/Resource/CircuitBreaker stuff. and for Arrow users, a
kap-arrow
module makes more sense — just bridge straight into Arrow types instead of duplicating logic (that usually ends up messy + harder to maintain anyway). that way you only pick what you actually need, nothing else bloating your dep graph. gonna look into this for the next version… thx for pushing on it, architecturally it’s def the right call 👍. I can start applying these changes
c
Yep, thanks! I think this way it's useful for more people
d
Hello! Do you want to check how it looks like with your suggestion? https://github.com/damian-rafael-lattenero/coroutines-applicatives
c
That seems much better! Different note, but you named the initial functions
lift1
,
lift2
,
lift3
etc. That naming is familiar to Haskell etc developers, but is not common in the JVM ecosystem. I would suggest a name that's less mathematics and more explicit on what it does, eg.
Copy code
kap(::Dashboard)
    .param { fetchUser() }
    .param { fetchCart() }
// etc
That may be too different from your vision though. Still, you don't need the
1
,
2
,
3
postfixes, the compiler can distinguish between overloads since the number of type parameters is different. You may need a
@JvmName
. See this example which has a much more complex signature.
Question: what if I want to evaluate parameters and have a barrier, but in another order than they are specified in the constructor? e.g. I want to compute the last two parameters first, then a barrier, then the rest of the parameters.
d
Hey Ivan! Took all your feedback seriously — here's where things landed. 1. No more
lift1
,
lift2
,
lift3
Single name:
kap
. The compiler resolves the overload by function arity (
Function2
,
Function3
, etc.) — no numeric suffixes, no `@JvmName`:
Copy code
kap(::Pair)           // compiler picks kap<P1, P2, R>
kap(::CheckoutResult) // compiler picks kap<P1, ..., P11, R>
// works up to 22 params Kotlin Multiplatform (JVM + JS + Native), so function types are enough to distinguish. 2.
.with
instead of
.ap
Reads like English — "kap this constructor with user, with cart, with promos":
Copy code
val checkout: CheckoutResult = Async {
    kap(::CheckoutResult)
        .with { fetchUser() }           // ┐
        .with { fetchCart() }            // ├─ phase 1: parallel
        .with { fetchPromos() }          // │
        .with { fetchInventory() }       // ┘
        .followedBy { validateStock() }  // ── phase 2: barrier
        .with { calcShipping() }         // ┐
        .with { calcTax() }              // ├─ phase 3: parallel
        .with { calcDiscounts() }        // ┘
        .followedBy { reservePayment() } // ── phase 4: barrier
        .with { generateConfirmation() } // ┐ phase 5
        .with { sendEmail() }            // ┘
}
3. Your question about executing in a different order than the constructor
"What if I want to compute the last two parameters first, then a barrier, then the rest?"
Two approaches depending on whether you need a barrier or not: If you just need different assembly order (no barrier) — all 4 run in parallel, you assemble the constructor however you want:
Copy code
data class Page(val a: String, val b: String, val c: String, val d: String)
val result = Async {
    combine(
        pair({ fetchC() }, { fetchD() }),
        pair({ fetchA() }, { fetchB() }),
    ) { (c, d), (a, b) -> Page(a, b, c, d) }
}
// All 4 run in parallel. Constructor receives them in its own order. If you need a barrier between phases — the
computation {}
builder is the cleanest way. Each
.bind()
acts as a barrier:
Copy code
val result = Async {
    computation {
        // Phase 1: c and d in parallel
        val (c, d) = pair({ fetchC() }, { fetchD() }).bind()
        // ── barrier (bind waits for phase 1) ──
        // Phase 2: a and b in parallel
        val (a, b) = pair({ fetchA() }, { fetchB() }).bind()
        // Assemble in constructor order
        Page(a, b, c, d)
    }
}
Within each
pair(...)
calls run in parallel, each
.bind()
is a barrier, and at the end you assemble the constructor in whatever order it expects. No nesting, no workarounds. Both examples compile and run verified in the repo. Full updated README + code: https://github.com/damian-rafael-lattenero/coroutines-applicatives
r
You should consider adding wasm targets (at least wasmJs). It's probably very simple change (you already have js support), but will make the library more useful for multiplatform projects.
d
@Robert Jaros Done! Added wasmJs target to kap-core and kap-resilience in commit 9c193b0. Thanks for the suggestion will be in the next Maven Central release. Browser and Node.js both supported. thanks so much
👍 1
@CLOVIS I made some updates! may interest you