如何转换ApplicativeError/MonadError的错误类型?
问题背景
我使用的某个库返回带有自定义错误类型的结果,该库为该结果类型定义了MonadError实例。为了和应用其余部分兼容,我需要转换错误类型,适配ApplicativeThrow或MonadThrow体系。
最初我尝试实现这样的函数:
def changeErrorType[F[_], A, E1, E2](x: F[A]) (convError: E1 => E2) (implicit ae1: MonadError[F, E1], ae2: MonadError[F, E2]): F[A] = x.attempt.flatMap(_.leftMap(convError).liftTo[F])
但很快发现这个方案不可行——同一F[_]类型无法同时拥有两个不同错误类型E1、E2的MonadError实例。于是我调整了函数签名:
def changeErrorType[F[_], G[_], A, E1, E2](x: F[A]) (convError: E1 => E2) (implicit ae1: MonadError[F, E1], ae2: MonadError[G, E2]): G[A]
但不知道该如何实现这个函数,想了解惯用的解决方案。
补充更新:
我可以把库的结果转换成Either,将自定义错误映射为Throwable,再把Either提升为ApplicativeThrow。但我想知道,如果应用其余部分都用ApplicativeThrow/MonadThrow,那库提供的MonadError[*, CustomErrorType]实例还有没有用?
惯用解决方案
1. 基于ApplicativeThrow的直接转换
这是最直接的适配方式,利用MonadError的redeem方法,在原类型F的上下文里处理成功和失败分支,直接转换为目标G类型(只需G满足ApplicativeThrow约束):
import cats.MonadError import cats.ApplicativeThrow def changeErrorType[F[_], G[_], A, E1, E2](fa: F[A]) (convError: E1 => E2) (implicit meF: MonadError[F, E1], atG: ApplicativeThrow[G]): G[A] = meF.redeem(fa, error => atG.raiseError(convError(error)), value => atG.pure(value) )
这个实现不需要G是MonadError,完全贴合应用使用ApplicativeThrow/MonadThrow的场景。
2. 用EitherT封装保留错误类型
如果需要在转换过程中保留错误类型的语义(比如后续还要针对E2做精细化处理),可以用EitherT包装结果,之后再按需转为G[A](此时要求E2是Throwable,符合MonadThrow的错误类型要求):
import cats.MonadError import cats.data.EitherT import cats.MonadThrow def wrapToEitherT[F[_], G[_], A, E1, E2](fa: F[A]) (convError: E1 => E2) (implicit meF: MonadError[F, E1], mG: Monad[G]): EitherT[G, E2, A] = EitherT(meF.attempt(fa).map(_.leftMap(convError))) // 转为MonadThrow兼容的G[A](E2需为Throwable) def toMonadThrow[F[_], G[_], A, E1, E2 <: Throwable](fa: F[A]) (convError: E1 => E2) (implicit meF: MonadError[F, E1], mtG: MonadThrow[G]): G[A] = wrapToEitherT(fa)(convError).merge
关于库MonadError实例的价值
即使应用全局使用ApplicativeThrow/MonadThrow,库提供的MonadError[*, CustomErrorType]实例依然有不可替代的作用:
- 编译期错误安全:在调用库的层级保留自定义错误类型,能在编译期检查错误处理的完整性,避免遗漏特定错误的处理逻辑,直到需要和全局错误体系兼容时再做转换。
- 精细化错误处理:可以利用
MonadError的handleErrorWith、ensure等方法,针对库的自定义错误做针对性恢复(比如只捕获特定的库错误并返回 fallback 值),而不是直接转为Throwable后统一处理。 - 操作组合流畅性:在库调用的上下文里组合多个库操作时,
MonadError能自动传递错误,无需手动处理Either,让代码更简洁。
举个实际使用的例子:
// 先针对库的自定义错误做精细化处理 import cats.MonadError val handledLibResult: F[A] = meF.handleErrorWith(libResult) { case SpecificLibError => meF.pure(fallbackValue) // 只处理特定错误 case otherErr => meF.raiseError(otherErr) // 其他错误继续传递 } // 最后转换为应用兼容的MonadThrow类型 val appResult: G[A] = changeErrorType(handledLibResult)(convErrorToThrowable)
内容的提问来源于stack exchange,提问作者Vladimir Korenev

