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
?
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() }
}