Scala Cats накапливает ошибки и успехи с Ior - PullRequest
2 голосов
/ 11 декабря 2019

Я пытаюсь использовать тип данных Cats Ior для накопления как ошибок, так и успехов использования службы (которая может возвращать ошибку).

def find(key: String): F[Ior[NonEmptyList[Error], A]] = {
  (for {
      b <- service.findByKey(key)
    } yield b.rightIor[NonEmptyList[Error]])
  .recover {
      case e: Error => Ior.leftNel(AnotherError)
    }
}

def findMultiple(keys: List[String]): F[Ior[NonEmptyList[Error], List[A]]] = {
  keys map find reduce (_ |+| _)
}

Моя путаница заключается в том, как объединить ошибки / успехи. Я пытаюсь использовать объединение полугруппы (инфиксный синтаксис) для объединения без успеха. Есть лучший способ это сделать? Любая помощь будет отличной.

1 Ответ

2 голосов
/ 12 декабря 2019

Я собираюсь предположить, что вам нужны все ошибки и все успешные результаты. Вот возможная реализация:

class Foo[F[_]: Applicative, A](find: String => F[IorNel[Error, A]]) {
  def findMultiple(keys: List[String]): F[IorNel[Error, List[A]]] = {
    keys.map(find).sequence.map { nelsList =>
      nelsList.map(nel => nel.map(List(_)))
        .reduceOption(_ |+| _).getOrElse(Nil.rightIor)
    }
  }
}

Давайте разберемся с ней:

Мы будем пытаться "перевернуть" List[IorNel[Error, A]] в IorNel[Error, List[A]]. Однако, выполнив keys.map(find), мы получим List[F[IorNel[...]]], поэтому нам нужно сначала «перевернуть» его аналогичным образом. Это можно сделать, используя .sequence для результата, и это то, что заставляет ограничение F[_]: Applicative.

NB Applicative[Future] доступно, когда существует неявная ExecutionContext область действия. Вы также можете избавиться от F и напрямую использовать Future.sequence.

Теперь у нас есть F[List[IorNel[Error, A]]], поэтому мы хотим map внутреннюю часть для преобразования nelsList, которое мы получили. Вы можете подумать, что sequence также может использоваться там, но это не может - он имеет поведение «короткое замыкание при первой ошибке», поэтому мы потеряем все успешные значения. Давайте попробуем использовать |+|.

Ior[X, Y] имеет экземпляр Semigroup, когда оба X и Y имеют его. Так как мы используем IorNel, X = NonEmptyList[Z], и это удовлетворено. Для Y = A - вашего типа домена - он может быть недоступен.

Но мы не хотим объединять все результаты в один A, нам нужно Y = List[A] (в котором также всегда есть полугруппа). Таким образом, мы берем каждое IorNel[Error, A], которое у нас есть, и map A в синглтоне List[A]:

nelsList.map(nel => nel.map(List(_)))

Это дает нам List[IorNel[Error, List[A]], которое мы можем уменьшить. К сожалению, поскольку у Иора нет Monoid, мы не можем использовать удобный синтаксис. Таким образом, с коллекциями stdlib можно сделать .reduceOption(_ |+| _).getOrElse(Nil.rightIor).


. Это можно улучшить, выполнив несколько вещей:

  1. x.map(f).sequence эквивалентно выполнению x.traverse(f)
  2. Мы можем требовать, чтобы ключи были непустыми заранее и также давали непустой результат.

Последний шаг дает нам Reducible экземпляр для коллекции, позволяя намсократите все, выполнив reduceMap

class Foo2[F[_]: Applicative, A](find: String => F[IorNel[Error, A]]) {
  def findMultiple(keys: NonEmptyList[String]): F[IorNel[Error, NonEmptyList[A]]] = {
    keys.traverse(find).map { nelsList =>
      nelsList.reduceMap(nel => nel.map(NonEmptyList.one))
    }
  }
}

Конечно, вы можете сделать из этого однострочную строку:

keys.traverse(find).map(_.reduceMap(_.map(NonEmptyList.one)))

Или же вы можете проверить не пустоту внутри:

class Foo3[F[_]: Applicative, A](find: String => F[IorNel[Error, A]]) {
  def findMultiple(keys: List[String]): F[IorNel[Error, List[A]]] = {
    NonEmptyList.fromList(keys)
      .map(_.traverse(find).map { _.reduceMap(_.map(List(_))) })
      .getOrElse(List.empty[A].rightIor.pure[F])
  }
}
...