假设我想编写一个带有以下签名的方法:
def parse(input: List[(String, String)]):
ValidationNel[Throwable, List[(Int, Int)]]
对于输入中的每对字符串,需要验证两个成员都可以解析为整数,并且第一个成员小于第二个成员。然后它需要返回整数,累积出现的任何错误。
首先,我将定义错误类型:
import scalaz._, Scalaz._
case class InvalidSizes(x: Int, y: Int) extends Exception(
s"Error: $x is not smaller than $y!"
)
现在我可以按如下方式实现我的方法:
def checkParses(p: (String, String)):
ValidationNel[NumberFormatException, (Int, Int)] =
p.bitraverse[
({ type L[x] = ValidationNel[NumberFormatException, x] })#L, Int, Int
](
_.parseInt.toValidationNel,
_.parseInt.toValidationNel
)
def checkValues(p: (Int, Int)): Validation[InvalidSizes, (Int, Int)] =
if (p._1 >= p._2) InvalidSizes(p._1, p._2).failure else p.success
def parse(input: List[(String, String)]):
ValidationNel[Throwable, List[(Int, Int)]] = input.traverseU(p =>
checkParses(p).fold(_.failure, checkValues _ andThen (_.toValidationNel))
)
或者,或者:
def checkParses(p: (String, String)):
NonEmptyList[NumberFormatException] \/ (Int, Int) =
p.bitraverse[
({ type L[x] = ValidationNel[NumberFormatException, x] })#L, Int, Int
](
_.parseInt.toValidationNel,
_.parseInt.toValidationNel
).disjunction
def checkValues(p: (Int, Int)): InvalidSizes \/ (Int, Int) =
(p._1 >= p._2) either InvalidSizes(p._1, p._2) or p
def parse(input: List[(String, String)]):
ValidationNel[Throwable, List[(Int, Int)]] = input.traverseU(p =>
checkParses(p).flatMap(s => checkValues(s).leftMap(_.wrapNel)).validation
)
现在无论出于何种原因,第一个操作(验证对解析为字符串)感觉对我来说就像验证问题一样,而第二个(检查值)感觉就像一个脱节问题,它感觉就像我需要单独组成两个(这表明我应该使用\/
,因为ValidationNel[Throwable, _]
没有monad实例)。
在我的第一个实现中,我在整个过程中使用ValidationNel
,然后在最后使用fold
作为一种假的flatMap
。在第二种情况下,我会在ValidationNel
和\/
之间来回反复,具体取决于我是否需要错误累积或monadic绑定。它们产生相同的结果。
我在实际代码中使用了这两种方法,并且还没有开发出对一种方法的偏好。我错过了什么吗? 应该我更喜欢一个吗?
答案 0 :(得分:9)
这可能不是您正在寻找的答案,但我只是注意到Validation
有以下方法
/** Run a disjunction function and back to validation again. Alias for `@\/` */
def disjunctioned[EE, AA](k: (E \/ A) => (EE \/ AA)): Validation[EE, AA] =
k(disjunction).validation
/** Run a disjunction function and back to validation again. Alias for `disjunctioned` */
def @\/[EE, AA](k: (E \/ A) => (EE \/ AA)): Validation[EE, AA] =
disjunctioned(k)
当我看到它们时,在我记住这个问题之前,我真的看不出它们的用处。它们允许您通过转换为析取来进行适当的绑定。
def checkParses(p: (String, String)):
ValidationNel[NumberFormatException, (Int, Int)] =
p.bitraverse[
({ type L[x] = ValidationNel[NumberFormatException, x] })#L, Int, Int
](
_.parseInt.toValidationNel,
_.parseInt.toValidationNel
)
def checkValues(p: (Int, Int)): InvalidSizes \/ (Int, Int) =
(p._1 >= p._2) either InvalidSizes(p._1, p._2) or p
def parse(input: List[(String, String)]):
ValidationNel[Throwable, List[(Int, Int)]] = input.traverseU(p =>
checkParses(p).@\/(_.flatMap(checkValues(_).leftMap(_.wrapNel)))
)
答案 1 :(得分:2)
以下是我的Cats代码第二版的非常接近的翻译:
import scala.util.Try
case class InvalidSizes(x: Int, y: Int) extends Exception(
s"Error: $x is not smaller than $y!"
)
def parseInt(input: String): Either[Throwable, Int] = Try(input.toInt).toEither
def checkValues(p: (Int, Int)): Either[InvalidSizes, (Int, Int)] =
if (p._1 >= p._2) Left(InvalidSizes(p._1, p._2)) else Right(p)
import cats.data.{EitherNel, ValidatedNel}
import cats.instances.either._
import cats.instances.list._
import cats.syntax.apply._
import cats.syntax.either._
import cats.syntax.traverse._
def checkParses(p: (String, String)): EitherNel[Throwable, (Int, Int)] =
(parseInt(p._1).toValidatedNel, parseInt(p._2).toValidatedNel).tupled.toEither
def parse(input: List[(String, String)]): ValidatedNel[Throwable, List[(Int, Int)]] =
input.traverse(fields =>
checkParses(fields).flatMap(s => checkValues(s).toEitherNel).toValidated
)
要更新此问题,此代码是“根据我需要错误累积还是单子绑定,在ValidatedNel
和Either
之间来回跳动。”
自从我问了这个问题以来的近六年中,Cats的introduced a Parallel
type class(在Cats 2.0.0中得到了改进)可以完全解决我遇到的问题:
import cats.data.EitherNel
import cats.instances.either._
import cats.instances.list._
import cats.instances.parallel._
import cats.syntax.either._
import cats.syntax.parallel._
def checkParses(p: (String, String)): EitherNel[Throwable, (Int, Int)] =
(parseInt(p._1).toEitherNel, parseInt(p._2).toEitherNel).parTupled
def parse(input: List[(String, String)]): EitherNel[Throwable, List[(Int, Int)]] =
input.parTraverse(fields =>
checkParses(fields).flatMap(checkValues(_).toEitherNel)
)
当我们要累积错误时,我们可以切换par
版的应用运算符,例如traverse
或tupled
,否则我们将在Either
中工作,这使我们有了单峰绑定,并且我们完全不必再引用Validated
。