是否有任何理由使用 suspend fun fn(...): Either<Throwable, A> 而不是 suspend fun fn(...): A?

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

我正在考虑有关 suspend 的内容,Arrow 的文档对此进行了详细解释:suspend () -> A 提供与 IO<A> 相同的保证。

因此,根据文档,仅使用 suspend 我们将不纯函数转换为纯函数:

不纯

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

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

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

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

仅添加 suspend 即可将函数变为纯函数这一事实令人惊讶,但 clearly explained in the doc

考虑到这一点,我的下一个疑问与可能导致错误 (Throwable) 或值 A.

的业务服务建模有关

到目前为止,我一直在做这样的事情:

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")
    }
}

但是,假设 suspend fun fn() : A 是纯的并且等价于 IO<A>,我们可以将上面的程序重写为:

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
    }
}

有什么理由更喜欢 suspend fun fn(...): Either<Throwable, A> 而不是暂停 fun fn(...): A

如果您想使用 Throwable,有 2 个选项,kotlin.Resultarrow.core.Either

最大的区别在runCatchingEither.catch之间。其中runCatching会捕获所有异常,而Either.catch只会捕获non-fatal exceptions。所以 Either.catch 会防止你误吞 kotlin.coroutines.CancellationException.

您应该将上面的代码更改为以下代码,因为 either { } 不会捕获任何异常。

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?

是的,在 return 类型中使用 ResultEither 的原因是强制调用者解决错误。强制用户解决错误,即使在 IO<A>suspend 内仍然很有价值,因为最后的 try/catch 是可选的。

但是使用 Either 对跟踪整个业务领域的错误变得真正有意义。或者以类型化的方式跨层解决它们。

例如:

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