Skip to content

Coming from Arrow

If you're already using Arrow's parZip for parallel execution, KAP can complement or replace it for multi-phase orchestration. This guide shows the migration path.

KAP + Arrow: complementary, not competing

KAP's kap-arrow module uses Arrow's Either and NonEmptyList. You don't have to choose one or the other:

dependencies {
    implementation("io.github.damian-rafael-lattenero:kap-core:3.0.0")
    implementation("io.github.damian-rafael-lattenero:kap-arrow:3.0.0") // uses Arrow Core
}

Simple parallel: parZipcombine

val result = parZip(
    { fetchUser() },
    { fetchCart() },
    { fetchPromos() },
) { user, cart, promos -> Dashboard(user, cart, promos) }
val result = combine(
    { fetchUser() },
    { fetchCart() },
    { fetchPromos() },
) { user, cart, promos -> Dashboard(user, cart, promos) }
    .evalGraph()

Almost identical. KAP's combine is Arrow's parZip equivalent.

Type-safe ordering: parZipkap + .with

// Named lambda params — swap user/cart? No compiler error.
val result = parZip(
    { fetchUser() },
    { fetchCart() },
) { user, cart -> Page(user, cart) }
@KapTypeSafe
data class Page(val user: String, val cart: String)

// Typed chain — swap .with lines? COMPILE ERROR.
val result = kap(::Page)
    .with { user from fetchUser() }
    .with { cart from fetchCart() }
    .evalGraph()

Multi-phase: sequential parZip → flat chain

This is where KAP shines. Arrow requires separate parZip calls with intermediate variables:

// Phase 1
val (user, cart) = parZip(
    { fetchUser() }, { fetchCart() },
) { u, c -> Pair(u, c) }

// Phase 2 (barrier — just a suspend call)
val validated = validate(user, cart)

// Phase 3
val (shipping, tax) = parZip(
    { calcShipping() }, { calcTax() },
) { s, t -> Pair(s, t) }

val result = Result(user, cart, validated, shipping, tax)
@KapTypeSafe
data class Result(val user: User, val cart: Cart, val validated: Validated, val shipping: Shipping, val tax: Tax)

val result = kap(::Result)
    .with { user from fetchUser() }           // ┐ phase 1
    .with { cart from fetchCart() }            // ┘
    .then { validated from validate() }       // ── phase 2: barrier
    .with { shipping from calcShipping() }    // ┐ phase 3
    .with { tax from calcTax() }              // ┘
    .evalGraph()

Value-dependent phases: nested parZip.andThen

val ctx = parZip(
    { fetchProfile(userId) }, { fetchPrefs(userId) },
) { profile, prefs -> UserContext(profile, prefs) }

// ctx needed for phase 2
val enriched = parZip(
    { fetchRecs(ctx.profile) }, { fetchPromos(ctx.prefs) },
) { recs, promos -> Enriched(recs, promos) }
@KapTypeSafe
data class UserContext(val profile: String, val prefs: String)
@KapTypeSafe
data class Enriched(val recs: String, val promos: String)

val enriched = kap(::UserContext)
    .with { profile from fetchProfile(userId) }
    .with { prefs from fetchPrefs(userId) }
    .andThen { ctx ->
        kap(::Enriched)
            .with { recs from fetchRecs(ctx.profile) }
            .with { promos from fetchPromos(ctx.prefs) }
    }
    .evalGraph()

Validation: zipOrAccumulatezipV

val result = Either.zipOrAccumulate(
    { validateName(name) },
    { validateEmail(email) },
    { validateAge(age) },
) { n, e, a -> User(n, e, a) }
val result = zipV(
    { validateName(name) },
    { validateEmail(email) },
    { validateAge(age) },
) { n, e, a -> User(n, e, a) }
    .evalGraph()
// Same error accumulation, but validators run in parallel, and scales to 22

Features KAP adds over Arrow

Feature Arrow KAP
Visible phases No .then / .andThen
Compile-time arg order No Typed function chain
Max arity 9 22
timeoutRace No Parallel fallback, 2.6x faster
raceQuorum No N-of-M consensus
.settled() No Partial failure tolerance
.memoizeOnSuccess() No Cache only successes
Parallel validation zipOrAccumulate zipV (parallel + arity 22)