如何优雅调用带多上下文接收器的Arrow.kt函数(替代Either)
优雅处理Arrow.kt多错误上下文接收器函数的调用
背景
Arrow.kt文档指出,我们可以用带上下文接收器的写法替代返回Either的函数:
原写法:
fun get(): Either<Err, Res>
替代写法:
context(Raise<Err>) fun get(): Res
该写法支持多个上下文接收器。比如定义两种错误类型:
class Err1 class Err2
即可写出同时携带两种错误上下文的函数:
context(Raise<Err1>, Raise<Err2>) private fun func(a: Int): Int { return when(a) { 0 -> raise(Err1()) 1 -> raise(Err2()) else -> a } }
核心问题
如何优雅调用该函数,无需深层嵌套就能处理部分或全部错误?目前常见的嵌套写法十分繁琐:
recover<Err1, Unit>({ recover<Err2, Unit>({ val res = func(1) // 正常处理逻辑 }) { err2 -> /* 处理Err2 */ } }){ err1 -> /* 处理Err1 */ }
拆分函数虽能消除嵌套,但会让代码更零散,得不偿失。
优雅解决方案
方案1:链式调用recover
Arrow支持将recover操作链式调用,把多错误处理逻辑平铺开来:
runCatchingRaises { val res = func(1) // 正常处理逻辑 } .recover { err: Err1 -> // 处理Err1 } .recover { err: Err2 -> // 处理Err2 }
这里的runCatchingRaises可使用Arrow提供的Either.catch,或自定义适配多Raise上下文的启动器,本质是将多错误上下文转换为可链式处理的错误流。
方案2:用密封类+fold统一处理
先将函数调用结果转换回Either类型,通过密封类统一错误类型,再用fold完成所有逻辑处理:
首先定义密封错误类:
sealed class AppErr { data class WrappedErr1(val err: Err1) : AppErr() data class WrappedErr2(val err: Err2) : AppErr() }
然后转换并处理:
val result = Either.catch<AppErr, Int> { func(1) } .fold( ifLeft = { err -> when(err) { is AppErr.WrappedErr1 -> /* 处理Err1 */ is AppErr.WrappedErr2 -> /* 处理Err2 */ } }, ifRight = { res -> /* 正常处理逻辑 */ } )
这种方式通过密封类收拢错误类型,用一个fold就能完成所有错误与正常逻辑的处理,完全避免嵌套。
方案3:封装扩展函数平铺处理逻辑
自定义扩展函数,将多错误处理逻辑作为参数传入,实现平铺式调用:
context(Raise<Err1>, Raise<Err2>) fun <R> handleErrors( onErr1: (Err1) -> R, onErr2: (Err2) -> R, block: context(Raise<Err1>, Raise<Err2>) () -> R ): R { return try { block() } catch (e: Err1) { onErr1(e) } catch (e: Err2) { onErr2(e) } }
调用时:
handleErrors( onErr1 = { /* 处理Err1 */ }, onErr2 = { /* 处理Err2 */ } ) { val res = func(1) // 正常处理逻辑 }
此方式将错误处理逻辑作为参数平铺,代码结构更清晰直观。
附言
带上下文接收器的Arrow.kt写法确实让人联想到Java的受检异常,区别在于这里的错误无需继承Throwable,且错误处理更灵活简洁。有意思的是,我们从Java的受检异常,到Kotlin取消受检异常,再到Arrow.kt用上下文接收器模拟类似受检异常的机制,像是绕了一圈又回到了某种形式的显式错误声明。
内容的提问来源于stack exchange,提问作者Alevinevich
相关产品推荐
相关产品推荐

