19

次のシグネチャを持つメソッドを書きたいとします。

def parse(input: List[(String, String)]):
  ValidationNel[Throwable, List[(Int, Int)]]

入力内の文字列のペアごとに、両方のメンバーが整数として解析できること、および最初のメンバーが 2 番目のメンバーよりも小さいことを確認する必要があります。次に、発生したエラーを累積して整数を返す必要があります。

まず、エラー タイプを定義します。

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
  )

なんらかの理由で、最初の操作 (ペアが文字列として解析されることの検証)は検証の問題のように感じます、2 番目の操作 (値のチェック)は論理和の問題のように感じますこれは、モナドインスタンスがないため、を使用する必要があることを示唆しています)。\/ValidationNel[Throwable, _]

私の最初の実装では、ValidationNel全体を通して使用foldし、最後に一種の偽物として使用しflatMapます。2 つ目では、エラーの蓄積が必要か、モナド バインドが必要かによって、 と の間を適切にValidationNel行き来します。\/それらは同じ結果を生成します。

私は実際のコードで両方のアプローチを使用してきましたが、まだどちらかを優先していません。何か不足していますか?どちらかを優先する必要がありますか

4

2 に答える 2

10

これはおそらくあなたが探している答えではありません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)))
  )
于 2013-11-22T00:10:55.327 に答える
2

以下は、 Catsのコードの 2 番目のバージョンのかなり近い翻訳です。

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

私がこの質問をしてからほぼ 6 年間で、Cats は、私が直面していた問題を正確に解決する型クラス( Cats 2.0.0で改善) を導入しました。Parallel

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のような適用演算子のバージョンを切り替えることができますが、それ以外の場合は で作業しているため、モナド バインディングが得られ、参照する必要がまったくありません。traversetupledEitherValidated

于 2019-09-11T06:59:21.433 に答える