使用Cats EitherT时返回子类IsPositive为何触发编译错误
最近在Scala里用Cats的EitherT处理异步逻辑时踩了个坑,想跟大家聊聊:
我写了个判断整数是否为负数的函数,当把返回类型声明为Future[Either[Error, Int]](Error是个trait)时,代码能正常编译;但改成用实际返回的具体错误类IsPositive作为Either的Left类型(也就是Future[Either[IsPositive, Int]])时,Scala直接抛出编译错误。
相关代码片段
import cats.data.EitherT import cats.implicits._ import scala.concurrent.Future // 定义错误体系 trait Error case class IsPositive() extends Error // 非负数时返回的错误 case class IsNegative() extends Error // 示例其他错误类 // 能正常编译的版本 def isNeg(i: Int): Future[Either[Error, Int]] = Future { if (i < 0) Right(i) else Left(IsPositive()) } // 编译失败的版本 def isNeg(i: Int): Future[Either[IsPositive, Int]] = Future { if (i < 0) Right(i) else Left(IsPositive()) }
为啥会编译失败?
我梳理了几个最可能的原因,你可以对照自己的代码排查:
1. 函数内部不小心返回了其他Error子类
如果你的isNeg函数里除了Left(IsPositive()),还不小心返回了其他Error的子类(比如Left(IsNegative())),编译器会自动推断这个Future块的返回类型是Either[Error, Int],但你声明的返回类型是Either[IsPositive, Int],类型不匹配自然就报错了。
这种情况很常见,比如写逻辑时手滑或者复制粘贴错了,检查下所有Left分支的返回实例就行。
2. EitherT组合操作时的类型不兼容
如果你的代码里把这个isNeg的结果转换成EitherT后,和其他EitherT实例(比如返回EitherT[Future, Error, String]的函数)进行组合(比如用for推导式、flatMap),问题就来了:
- 当
isNeg返回Future[Either[Error, Int]]时,对应的EitherT类型是EitherT[Future, Error, Int],和其他Left类型为Error的EitherT可以无缝组合; - 但改成
IsPositive后,EitherT类型变成EitherT[Future, IsPositive, Int],和EitherT[Future, Error, _]的Left类型不一致,Scala编译器无法自动完成类型统一,就会报错。
解决办法也简单:要么把其他EitherT的Left类型也改成IsPositive(如果业务逻辑允许),要么把当前的EitherT向上转型为Left类型为Error的版本,用Cats提供的widen方法就行:
val compatibleEitherT: EitherT[Future, Error, Int] = EitherT(isNeg(i)).widen[Error]
3. 缺少必要的Cats类型类实例
如果你的代码中使用了依赖Left类型的Cats方法(比如recover、handleErrorWith),这些方法需要Left类型对应有类型类实例(比如Monoid、Semigroup)。
- 如果你之前为
Errortrait提供过相关实例,那用Error作为Left类型时没问题; - 但换成
IsPositive后,若没有为它提供对应的实例,编译器就会找不到所需的隐式值,导致编译失败。
这种情况的话,只要为IsPositive实现对应的类型类实例就行,比如实现Monoid:
import cats.Monoid implicit val monoidIsPositive: Monoid[IsPositive] = new Monoid[IsPositive] { override def empty: IsPositive = IsPositive() override def combine(x: IsPositive, y: IsPositive): IsPositive = x }
总结
先从最直观的地方排查:检查函数内部的所有Left返回实例是否都是IsPositive;然后看是否有跨不同Left类型的EitherT组合操作;最后再确认是否缺少必要的类型类实例。一般来说,前两个原因是最常见的~
内容的提问来源于stack exchange,提问作者ps0604

