在 Scala 中以一种纯粹且安全的方式进行错误转换
Error conversion in Scala in a pure and safe way
我正在使用 cats-effect
来暂停副作用,并且在实现避免容易出错的 Throwable
s 的纯函数时遇到了困难。问题是 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
}
}
)
}
不幸的是,这似乎不可靠且容易出错。在有效的实现中,我们可以自由地抛出任何类型的 throwable,它们将被报告为 UnknownError
.
这似乎并不比简单地使用 Throwable
和 try-catch
更可靠。任何人都可以建议 better/safer 处理错误的技术吗?
当您身处 IO
的阴暗世界时,无法回避 Throwable
可能发生的事实。关键是要区分真正 异常 的错误和 预期.
的错误
尝试构建可能在野外发生的错误的类型化模型是一项永无止境的探索,因此我的建议是不要尝试。相反,决定要具体化到 API 中的错误,并允许任何其他错误在 IO
中以 Throwables
的形式出现,这样调用者就可以决定他们是否要处理以及在何处处理强制处理预期错误时的异常情况。
您的场景的一个非常简单的示例可能是:
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
) 并且可以在别处处理。
我正在使用 cats-effect
来暂停副作用,并且在实现避免容易出错的 Throwable
s 的纯函数时遇到了困难。问题是 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
}
}
)
}
不幸的是,这似乎不可靠且容易出错。在有效的实现中,我们可以自由地抛出任何类型的 throwable,它们将被报告为 UnknownError
.
这似乎并不比简单地使用 Throwable
和 try-catch
更可靠。任何人都可以建议 better/safer 处理错误的技术吗?
当您身处 IO
的阴暗世界时,无法回避 Throwable
可能发生的事实。关键是要区分真正 异常 的错误和 预期.
尝试构建可能在野外发生的错误的类型化模型是一项永无止境的探索,因此我的建议是不要尝试。相反,决定要具体化到 API 中的错误,并允许任何其他错误在 IO
中以 Throwables
的形式出现,这样调用者就可以决定他们是否要处理以及在何处处理强制处理预期错误时的异常情况。
您的场景的一个非常简单的示例可能是:
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
) 并且可以在别处处理。