Обработка нескольких типов ошибок через / disjunction Scala - PullRequest
3 голосов
/ 20 февраля 2020

У меня есть 3 отдельных модуля, каждый со своим собственным типом ошибки. Ниже приведена очень упрощенная версия.

object ModuleA {
  case class ErrorA(msg: String)
  def getA: ErrorA \/ String = "1".right
}

object ModuleB {
  case class ErrorB(msg: String)
  def getB(s: String): ErrorB \/ Int = 1.right
}

object ModuleC {
  case class ErrorC(msg: String)
  def getC(s: String, i: Int): ErrorC \/ Long = 1L.right
}

Как клиент этих модулей, какой лучший способ связать эти вызовы?

Первый - глубоко вложенный, сложный тип возвращаемого значения, но имеет все необходимые типы.

def call1: ModuleA.ErrorA \/ (ModuleB.ErrorB \/ (ModuleC.ErrorC \/ Long)) = {
  ModuleA.getA.map { s =>
    ModuleB.getB(s).map { i =>
      ModuleC.getC(s, i)
    }
  }
}

Второй - очень читаемый, но типы ошибок теряются (Предполагаемый тип возвращаемого значения - Product \/ Long ). В идеале хотелось бы что-то подобное с типами ошибок

def call2  =
  for {
    s  <- ModuleA.getA
    i  <- ModuleB.getB(s)
    l  <- ModuleC.getC(s, i)
  } yield l

Третье - Определите новые типы ошибок для инкапсуляции существующих. Это кажется невозможным для разных комбинаций

Наконец, попытался использовать EitherT, но, похоже, получился сложный

1 Ответ

3 голосов
/ 20 февраля 2020

Подумайте о создании типа данных алгебры c из ошибок, например

sealed abstract class Error(val message: String)
case class ErrorA(msg: String) extends Error(msg)
case class ErrorB(msg: String) extends Error(msg)
case class ErrorC(msg: String) extends Error(msg)

, а затем измените левую часть возвращаемых \/ на Error

import scalaz.\/
import scalaz.syntax.either._

object ModuleA {
  def getA: Error \/ String = "1".right
}

object ModuleB {
  def getB(s: String): Error \/ Int = ErrorB("boom").left
}

object ModuleC {
  def getC(s: String, i: Int): Error \/ Long = 1L.right
}

for {
  s  <- ModuleA.getA
  i  <- ModuleB.getB(s)
  l  <- ModuleC.getC(s, i)
} yield l

, что дает

res0: Error \/ Long = -\/(ErrorB(boom))

Если вы не можете создать ADT, рассмотрите leftMap, чтобы изменить тип ошибки на общий тип, например

case class ErrorWrapper(m: String)

for {
  s  <- ModuleA.getA.leftMap { e: ModuleA.ErrorA => ErrorWrapper(e.msg) }
  i  <- ModuleB.getB(s).leftMap { e: ModuleB.ErrorB => ErrorWrapper(e.msg) }
  l  <- ModuleC.getC(s, i).leftMap { e: ModuleC.ErrorC => ErrorWrapper(e.msg) }
} yield l
// res0: ErrorWrapper \/ Long = -\/(ErrorWrapper(boom))

или, может быть, даже необычно, через структурную типизацию

implicit class CommonErrorWrapper[A <: Product { def msg: String }](e: A) {
  def toErrorWrapper: ErrorWrapper = ErrorWrapper(e.msg)
}

for {
  s  <- ModuleA.getA.leftMap(_.toErrorWrapper)
  i  <- ModuleB.getB(s).leftMap(_.toErrorWrapper)
  l  <- ModuleC.getC(s, i).leftMap(_.toErrorWrapper)
} yield l
// res1: ErrorWrapper \/ Long = -\/(ErrorWrapper(boom))

leftMap полезно не только для изменения типа ошибки, но также мы можем обогатить ошибку, добавив локально доступную контекстную информацию.


Примечание * Монадный преобразователь EitherT может использоваться, когда форма имеет тип F[A \/ B], например, Future[Error \/ B], однако в вашем случае это просто A \/ B, следовательно, EitherT это не может быть правильным инструментом. Смежный вопрос EitherT с несколькими типами возврата

...