I’m experimenting with raise DSL and error accumul...
# arrow
t
I’m experimenting with raise DSL and error accumulation with error remapping, but I’m not happy with neither of the options I seem to have:
Copy code
// 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!
a
there's an
accumulate
DSL for this, but since it's experimental the docs are not very good yet 🙈
I'll try to send a snippet later today
❤️ 1
y
I think what you're looking for is
withError
, 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.
So your example would just be:
Copy code
context(_: 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):
Copy code
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!
🙏 1
t
thanks @Youssef Shoaib [MOD],
withError
is better!
@Alejandro Serrano.Mena I refactored to accumulate DSL and it’s a really nice improvement:
Copy code
context(_: 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
            )
        }
    }
🎉 1
y
Even better with property delegation:
Copy code
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) }

            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:
Copy code
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
.
t
nice, didn’t notice the property delegation
I know,
zipOrAccumulated
is limited and verbose, accumulate dsl is a really nice addition.