Преобразование ошибок в Scala чистым и безопасным способом - PullRequest
0 голосов
/ 22 февраля 2019

Я использую cats-effect для приостановки побочных эффектов и столкнулся с трудностями при реализации чистых функций, избегая подверженных ошибкам Throwable s.Проблема в том, что cats.effect.Sync[F[_]] extends 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 s с try-catch.Кто-нибудь может предложить лучшую / более безопасную технику для устранения ошибок?

1 Ответ

0 голосов
/ 22 февраля 2019

Когда вы находитесь в мирном мире IO, от того, что Throwable s может случиться, не уйдет.Ключ в том, чтобы различать ошибки, которые действительно исключительные , и ошибки ожидаемые .

Это бесконечный квест, чтобы попытаться построить типизированную модельвозможные ошибки, которые могут произойти в дикой природе, поэтому мой совет не пытаться.Скорее, определите ошибки, которые вы хотите преобразовать в 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) и потенциально могут быть обработаны в другом месте.

...