【问题标题】:Scala Cats Accumulating Errors and Successes with IorScala Cats 使用 Ior 累积错误和成功
【发布时间】:2020-04-04 22:54:33
【问题描述】:

我正在尝试使用 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 (_ |+| _)
}

我的困惑在于如何组合错误/成功。我正在尝试使用 Semigroup 组合(中缀语法)来组合但没有成功。有一个更好的方法吗?任何帮助都会很棒。

【问题讨论】:

    标签: scala scala-cats


    【解决方案1】:

    我假设您想要所有错误和所有成功的结果。这是一个可能的实现:

    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 约束的原因。

    注意只要在范围内有隐式 ExecutionContextApplicative[Future] 就可用。也可以去掉F,直接使用Future.sequence

    现在,我们有了F[List[IorNel[Error, A]]],所以我们想要将map 内部部分转换为我们得到的nelsList。你可能认为sequence 也可以在那里使用,但它不能——它具有“第一个错误时短路”的行为,所以我们会丢失所有成功的值。让我们尝试改用|+|

    Ior[X, Y] 有一个 Semigroup 实例,而 XY 都有一个。由于我们使用的是IorNelX = 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]],我们可以减少它。不幸的是,由于 Ior 没有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])
      }
    }
    

    【讨论】:

      【解决方案2】:

      Ior 是一个很好的警告积累选择,即错误和成功的值。但是,正如 Oleg Pyzhcov 所提到的,Ior.Left 案例是短路的。这个例子说明了这一点:

      scala> val shortCircuitingErrors = List(
        Ior.leftNec("error1"),
        Ior.bothNec("warning2", 2),
        Ior.bothNec("warning3", 3)
      ).sequence
      
      shortCircuitingErrors: Ior[Nec[String], List[Int]]] = Left(Chain(error1))
      

      累积错误和成功的一种方法是将所有Left 案例转换为Both。一种方法是使用Option 作为正确类型并将Left(errs) 值转换为Both(errs, None)。调用.traverse 后,您最终会在右侧得到optList: List[Option],您可以使用optList.flatMap(_.toList) 将其展平以过滤掉None 值。

      class Error
      class KeyValue
      
      def find(key: String): Ior[Nel[Error], KeyValue] = ???
      
      def findMultiple(keys: List[String]): Ior[Nel[Error], List[KeyValue]] =
        keys
          .traverse { k =>
            val ior = find(k)
            ior.putRight(ior.right)
          }
          .map(_.flatMap(_.toList))
      

      或者更简洁:

      def findMultiple(keys: List[String]): Ior[Nel[Error], List[KeyValue]] =
        keys.flatTraverse { k =>
          val ior = find(k)
          ior.putRight(ior.toList) // Ior[A,B].toList: List[B]
        }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-26
        • 2019-04-09
        相关资源
        最近更新 更多