1

Arrow.kt docs point out that instead of:

fun get(): Either<Err, Res>

we can use

context(Raise<Err>)
fun get(): Res

And we can even have multiple context receivers.

Imagine we have two error types

class Err1
class Err2

And a function that has two context receivers of those errors

context(Raise<Err1>, Raise<Err2>)
private fun func(a: Int): Int {
    return when(a) {
        0 -> raise(Err1())
        1 -> raise(Err2())
        else -> a
    }
}

How this function can be elegantly called to handle many or all errors without deep nesting?

Only ugly nesting way comes to my mind:

recover<Err1, Unit>({
    recover<Err2, Unit>({
        val res = func(1)
        //happy pass
    }) { err2 -> /* error 1 */ }
}){ err1 -> /* error 2 */ }

I can remove nesting by creating separate functions and recovering only once per function but that arguably would be even worse.

P.S. Arrow.kt with context receivers awfully reminds me how checked exceptions work in java. Except that your error is not required to inherit Throwable and the fact that all exceptions can be caught much cleaner.
That's a bit funny that we came a full circle from Java checked exceptions to Kotlin's no checked exceptions to Arrow.kt way to 'emulate' checked exceptions with context receivers :)

  • I'm not familiar with Arrow (I try to avoid it as it adds more complexity than what it simplifies in my eyes). Why not just returning the Either and handle the errors on the caller? This doesn't feel like a good situation to use context receivers. I would also try to make both errors a sealed class so I can do a when and dispatch the right function to handle them. Just to be clear the idea of not having exceptions doesn't come from Arrow or Kotlin, it comes from vanilla FP languages. – Augusto Jun 12 '23 at 20:38

1 Answers1

0

As Agusto mentioned, try using "sealed interfaces".

So:

sealed interface Err {
    class Err1 : Err
    class Err2 : Err
}

context(Raise<Err>)
private fun func(a: Int): Int {
    return when (a) {
        0 -> raise(Err.Err1())
        1 -> raise(Err.Err2())
        else -> a
    }
}

And:

recover<Err, Unit>({
    val res = func(1)
    //happy pass
}) { err ->
    when (err) {
        is Err.Err1 -> /* error 1 */
        is Err.Err2 -> /* error 2 */
    }
}

Personally, for "func", I prefer:

context(Raise<Err>)
private fun func(a: Int): Int {
    ensure(a != 0) { Err.Err1() }
    ensure(a != 1) { Err.Err2() }
    return a
}