我使用cats-effect来挂起副作用,在实现纯函数时遇到了困难,避免了容易出错的Throwable,问题是cats.effect.Sync[F[_]]扩展了Bracket[F, Throwable]。
sealed trait Err
final case class FileExistError(path: String) extends Err
case object UnknownError extends Err
final case class FileExistThrowable(path: String, cause: Throwable) extends Throwable
final class File[F[_]: Sync]{
def rename(from: String, to: String): F[Unit] =
implicitly[Sync[F]] delay {
try{
Files.move(Paths.get(from), Paths.get(to))
} catch {
case e: FileAlreadyExistsException =>
throw FileExistThrowable(to, e)
case e => throw e
}
}
}在例如cats.effect.IO的情况下,我可以使用NaturalTransform转换效果,如下所示:
implicit val naturalTransform: IO ~> EitherT[IO, Err, ?] =
new ~>[IO, EitherT[IO, Err, ?]] {
override def apply[A](fa: IO[A]): EitherT[IO, Err, A] =
EitherT(
fa.attempt map { e =>
e.left map {
case FileExistsThrowable(path, cause) =>
FileExistsError(path)
case NonFatal(e) =>
UnknownError
}
}
)
}不幸的是,这似乎不可靠,而且容易出错。在有效的实现中,我们可以随意抛出任何类型的抛出,这些抛出将被报告为UnknownError。
比起简单地将Throwable与try-catch一起使用,这似乎不太可靠。有人能建议一种更好/更安全的方法来处理错误吗?
发布于 2019-02-22 16:54:33
当您身处IO这个混浊的世界时,无法摆脱Throwable可能发生的事实。关键是区分真正异常的错误和预期的错误。
这是一个永无止境的追求,试图建立一个可能发生在野外的错误的类型模型,所以我的建议是不要尝试。相反,决定要将哪些错误具体化为API,并允许任何其他错误以Throwables的形式在IO中发生,这样调用方就可以在执行预期错误处理的同时,决定是否和在何处处理异常情况。
您的场景的一个非常简单的示例可以是:
final case class FileAlreadyExists(path: String)
final class File[F[_]: Sync]{
def rename(from: String, to: String): F[Either[FileAlreadyExists, Unit]] =
Sync[F].delay { Files.move(Paths.get(from), Paths.get(to))}.attempt.flatMap {
case Left(_ : FileAlreadyExistsException) => Sync[F].pure(Left(FileAlreadyExists(to)))
case Left(e) => Sync[F].raiseError(e)
case Right(_) => Sync[F].pure(Right(()))
}
}通过这种方式,您可以区分将文件重命名为已存在的文件的预期错误(该错误发生在Either中并具有良好的类型)和完全出乎意料的错误(仍然发生在IO中),并可能在其他地方处理。
https://stackoverflow.com/questions/54827029
复制相似问题