Search code examples
kotlinarrow-kt

Is there any reason to use suspend fun fn(...): Either<Throwable, A> instead of suspend fun fn(...): A?


I'm mulling over something regarding suspend that Arrow's documentation explains in detail: suspend () -> A offers the same guaranties as IO<A>.

So, according to the documentation, just using suspend we are converting our impure functions into pure functions:

Impure

fun log(message: String): Unit = println(message)

fun main(): Unit { 
  log("Hey!")
}

Pure

suspend fun log(message: String): Unit = println(message)

fun main(): Unit = runBlocking { 
  log("Hey!")
}

The fact that just adding suspend turns the function into pure was surprising but was clearly explained in the doc.

Considering this, my next doubt is related to the modelling of business services that could result in an error (Throwable) or in a value A.

Up to now I was doing something like this:

suspend fun log(message: String): Either<Throwable, Unit> = either { println(message) }
suspend fun add(sum1: Int, sum2: Int): Either<Throwable, Int> = either { sum1 + sum2 }

suspend fun main() {
    val program = either<Throwable, Unit> {
        val sum = add(1, 2).bind()
        log("Result $sum").bind()
    }
    when(program) {
        is Either.Left -> throw program.value
        is Either.Right -> println("End")
    }
}

BUT, given that suspend fun fn() : A is pure and equivalent to IO<A>, we could rewrite the above program as:

suspend fun add(sum1: Int, sum2: Int): Int = sum1 + sum2
suspend fun log(message: String): Unit = println(message)

fun main() = runBlocking {
    try {
        val sum = add(1, 2)
        log("Result $sum")
    } catch( ex: Throwable) {
        throw ex
    }
}

Is there any reason to prefer suspend fun fn(...): Either<Throwable, A> over suspend fun fn(...): A?


Solution

  • If you want to work with Throwable there are 2 options, kotlin.Result or arrow.core.Either.

    The biggest difference is between runCatching and Either.catch. Where runCatching will capture all exceptions, and Either.catch will only catch non-fatal exceptions. So Either.catch will prevent you from accidentally swallowing kotlin.coroutines.CancellationException.

    You should change the code above to the following, because either { } doesn't catch any exceptions.

    suspend fun log(message: String): Either<Throwable, Unit> =
      Either.catch { println(message) }
    
    suspend fun add(sum1: Int, sum2: Int): Either<Throwable, Int> =
      Either.catch { sum1 + sum2 }
    

    Is there any reason to prefer suspend fun fn(...): Either<Throwable, A> over suspend fun fn(...): A?

    Yes, the reason to use Result or Either in the return type would be to force the caller to resolve the errors. Forcing the user to resolve an error, even within IO<A> or suspend is still valuable since try/catch at the end is optional.

    But using Either becomes truly meaningful to track errors across your entire business domain. Or resolve them across layers but in a typed way.

    For example:

    data class User(...)
    data class UserNotFound(val cause: Throwable?)
    
    fun fetchUser(): Either<UserNotFound, User> = either {
      val user = Either.catch { queryOrNull("SELECT ...") }
       .mapLeft { UserNotFound(it) }
       .bind()
      ensureNotNull(user) { UserNotFound() }
    }