Scala Cats 使用 Ior 积累错误和成功
Scala Cats Accumulating Errors and Successes with Ior
我正在尝试使用 Cats 数据类型 Ior 来累积使用服务的错误和成功(这可能 return 一个错误)。
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 (_ |+| _)
}
我的困惑在于如何组合errors/successes。我正在尝试使用 Semigroup 组合(中缀语法)组合但没有成功。有一个更好的方法吗?任何帮助都会很棒。
我假设您想要所有错误和所有成功结果。这是一个可能的实现:
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)
}
}
}
让我们分解一下:
我们将尝试"flip"一个List[IorNel[Error, A]]
变成IorNel[Error, List[A]]
。但是,通过 keys.map(find)
我们得到 List[F[IorNel[...]]]
,因此我们还需要先以类似的方式 "flip" 它。这可以通过在结果上使用 .sequence
来完成,这就是强制 F[_]: Applicative
约束的原因。
N.B。只要范围内存在隐式 ExecutionContext
,Applicative[Future]
就可用。也可以去掉F
,直接用Future.sequence
。
现在,我们有 F[List[IorNel[Error, A]]]
,所以我们想 map
内部来转换我们得到的 nelsList
。你可能认为 sequence
也可以在那里使用,但它不能 - 它有 "short-circuit on first error" 行为,所以我们会失去所有成功的价值。让我们尝试使用 |+|
代替。
当 X
和 Y
都有一个时,Ior[X, Y]
有一个 Semigroup
实例。由于我们使用的是 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]]
,我们可以减少它。不幸的是,由于 Ior 没有 Monoid
,我们不能完全使用方便的语法。因此,对于 stdlib 集合,一种方法是 .reduceOption(_ |+| _).getOrElse(Nil.rightIor)
.
这可以通过做几件事来改善:
x.map(f).sequence
等价于x.traverse(f)
- 我们可以预先要求键是非空的,并且返回非空结果。
后一步为我们提供了集合的 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])
}
}
Ior是警告积累的好选择,即errors和successful值。但是,正如 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]
}
我正在尝试使用 Cats 数据类型 Ior 来累积使用服务的错误和成功(这可能 return 一个错误)。
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 (_ |+| _)
}
我的困惑在于如何组合errors/successes。我正在尝试使用 Semigroup 组合(中缀语法)组合但没有成功。有一个更好的方法吗?任何帮助都会很棒。
我假设您想要所有错误和所有成功结果。这是一个可能的实现:
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)
}
}
}
让我们分解一下:
我们将尝试"flip"一个List[IorNel[Error, A]]
变成IorNel[Error, List[A]]
。但是,通过 keys.map(find)
我们得到 List[F[IorNel[...]]]
,因此我们还需要先以类似的方式 "flip" 它。这可以通过在结果上使用 .sequence
来完成,这就是强制 F[_]: Applicative
约束的原因。
N.B。只要范围内存在隐式 ExecutionContext
,Applicative[Future]
就可用。也可以去掉F
,直接用Future.sequence
。
现在,我们有 F[List[IorNel[Error, A]]]
,所以我们想 map
内部来转换我们得到的 nelsList
。你可能认为 sequence
也可以在那里使用,但它不能 - 它有 "short-circuit on first error" 行为,所以我们会失去所有成功的价值。让我们尝试使用 |+|
代替。
X
和 Y
都有一个时,Ior[X, Y]
有一个 Semigroup
实例。由于我们使用的是 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]]
,我们可以减少它。不幸的是,由于 Ior 没有 Monoid
,我们不能完全使用方便的语法。因此,对于 stdlib 集合,一种方法是 .reduceOption(_ |+| _).getOrElse(Nil.rightIor)
.
这可以通过做几件事来改善:
x.map(f).sequence
等价于x.traverse(f)
- 我们可以预先要求键是非空的,并且返回非空结果。
后一步为我们提供了集合的 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])
}
}
Ior是警告积累的好选择,即errors和successful值。但是,正如 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]
}