Tiberiu Tofan
11/17/2025, 4:52 PM// We're converting from Raise context -> Either -> back to Raise context, which is unnecessary overhead.
// The mapLeft->bind pattern obscures that we're just transforming the error type before re-raising it.
context(_: Raise<Int>)
fun testAccumulateAndMapLeftWithEither(): String =
either {
zipOrAccumulate(
{ raise("a") },
{ raise("b") },
{ raise("c") },
{ raise("d") }
) { _, _, _, _ -> "success" }
}.mapLeft { errors -> errors.size }.bind()
// Using `recover` is misleading because we're not actually recovering from the error.
// We're just transforming/remapping it to a different error type.
// The name "recover" suggests we're handling the error and continuing, but we're immediately re-raising.
context(_: Raise<Int>)
fun testAccumulateAndMapLeftWithRecover(): String =
recover(
block = {
zipOrAccumulate(
{ raise("a") },
{ raise("b") },
{ raise("c") },
{ raise("d") }
) { _, _, _, _ -> "success" }
},
recover = { errors ->
raise(errors.size)
}
)
do you have other suggestions? Thanks!Alejandro Serrano.Mena
11/17/2025, 5:09 PMaccumulate DSL for this, but since it's experimental the docs are not very good yet 🙈Alejandro Serrano.Mena
11/17/2025, 5:09 PMYoussef Shoaib [MOD]
11/17/2025, 5:17 PMwithError, which is generally useful beyond error accumulation. In general withError is like mapLeft for Either and mapError for Effect . In fact, withError is written almost identically to your recover-raise combo.Youssef Shoaib [MOD]
11/17/2025, 5:30 PMcontext(_: Raise<Int>)
fun testAccumulateAndMapLeftWithRecover(): String = withError(Nel<String>::size) {
zipOrAccumulate(
{ raise("a") },
{ raise("b") },
{ raise("c") },
{ raise("d") }
) { _, _, _, _ -> "success" }
}
or with the accumulate DSL (I think this compiles, but I haven't tested it):
context(_: Raise<Int>)
fun testAccumulateAndMapLeftWithRecover(): String = withError(Nel<String>::size) {
accumulate { // Maybe an `accumulateWithError` function might be nice to have in Arrow, although you can easily write it yourself
accumulate("a")
accumulate("b")
accumulate("c")
accumulate("d")
"success"
}
}
Which is ultimately equivalent to raise(4), as you'd expect.
You can also get both the errors and the return value if the return value didn't "force" any of the errors (this is the advantage of accumulate vs mapOrAccumulate) by using iorAccumulate (which, in this case, would return Ior.Both(4, "success"))
Feel free to ask any more accumulate or raise questions!Tiberiu Tofan
11/17/2025, 6:44 PMwithError is better!Tiberiu Tofan
11/19/2025, 6:28 PMcontext(_: Raise<CategorizedTransactionCorrupted>)
fun CategorizedTransactionEntity.toDomain(): CategorizedTransaction =
withError(::CategorizedTransactionCorrupted) {
accumulate {
val validTransactionId = accumulating { TransactionId(transactionId) }
val validClientId = accumulating { ClientId(clientId) }
val validAccountId = accumulating { AccountId(accountId) }
val validMoney = accumulating { Money(amount, currencyCode) }
val validMcc = accumulating { MerchantCategoryCode(mcc) }
val validExpenseCategory = accumulating { ExpenseCategory(expenseCategory) }
CategorizedTransaction(
id = CategorizedTransactionId(id),
transaction = Transaction(
id = validTransactionId.value,
clientId = validClientId.value,
accountId = validAccountId.value,
money = validMoney.value,
mcc = validMcc.value
),
expenseCategory = validExpenseCategory.value
)
}
}Youssef Shoaib [MOD]
11/19/2025, 6:35 PMcontext(_: Raise<CategorizedTransactionCorrupted>)
fun CategorizedTransactionEntity.toDomain(): CategorizedTransaction =
withError(::CategorizedTransactionCorrupted) {
accumulate {
val validTransactionId by accumulating { TransactionId(transactionId) }
val validClientId by accumulating { ClientId(clientId) }
val validAccountId by accumulating { AccountId(accountId) }
val validMoney by accumulating { Money(amount, currencyCode) }
val validMcc by accumulating { MerchantCategoryCode(mcc) }
val validExpenseCategory by accumulating { ExpenseCategory(expenseCategory) }
CategorizedTransaction(
id = CategorizedTransactionId(id),
transaction = Transaction(
id = validTransactionId,
clientId = validClientId,
accountId = validAccountId,
money = validMoney,
mcc = validMcc
),
expenseCategory = validExpenseCategory
)
}
}
Importantly, it has the flexibility to allow for more complex relationships than zipOrAccumulate. You get to decide when a variable is absolutely necessary for the computation (and thus raising if it failed).
For instance, imagine if Transaction and CategorizedTransactionId can both fail. You can write something like this then:
context(_: Raise<CategorizedTransactionCorrupted>)
fun CategorizedTransactionEntity.toDomain(): CategorizedTransaction =
withError(::CategorizedTransactionCorrupted) {
accumulate {
val validTransactionId by accumulating { TransactionId(transactionId) }
val validClientId by accumulating { ClientId(clientId) }
val validAccountId by accumulating { AccountId(accountId) }
val validMoney by accumulating { Money(amount, currencyCode) }
val validMcc by accumulating { MerchantCategoryCode(mcc) }
val validExpenseCategory by accumulating { ExpenseCategory(expenseCategory) }
val catId by accumulating { CategorizedTransactionId(id) }
val transaction by accumulating {
Transaction(
id = validTransactionId,
clientId = validClientId,
accountId = validAccountId,
money = validMoney,
mcc = validMcc
)
}
CategorizedTransaction(
id = catId,
transaction = transaction,
expenseCategory = validExpenseCategory
)
}
}
You can't easily do that with zipOrAccumulate.Tiberiu Tofan
11/19/2025, 6:46 PMTiberiu Tofan
11/19/2025, 6:49 PMzipOrAccumulated is limited and verbose, accumulate dsl is a really nice addition.