我正在考虑一些关于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
就把函数变成纯函数的事实令人惊讶,但在文档中有明确的解释。
考虑到这一点,我的下一个疑问与可能导致错误(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.Result
或arrow.core.Either
。
差异最大的是runCatching
和Either.catch
。其中runCatching
将捕获所有异常,而Either.catch
将只捕获非致命异常。所以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 }
是否有理由选择suspend fun fn(…):Either<Throwable,>over suspend fun(…):A?
是的,在返回类型中使用Result
或Either
的原因将是强制调用者解决错误。强制用户解决错误,即使在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() }
}