Валидация против дизъюнкции
Предположим, я хочу написать метод со следующей сигнатурой:
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, _]
не имеет экземпляра монады).
В моей первой реализации я использую ValidationNel
во всем, а затем fold
в конце как своего рода подделка flatMap
, Во втором я подпрыгиваю между ValidationNel
а также \/
в зависимости от того, нужно ли мне накопление ошибок или монадическое связывание. Они дают одинаковые результаты.
Я использовал оба подхода в реальном коде и еще не разработал предпочтения одного над другим. Я что-то пропустил? Должен ли я предпочесть один другому?
1 ответ
Это, вероятно, не тот ответ, который вы ищете, но я только что заметил 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)))
)
Ниже приводится довольно близкий перевод второй версии моего кода для 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
в зависимости от того, нужно ли мне накопление ошибок или монадическое связывание ".
За почти шесть лет, прошедших с тех пор, как я задал этот вопрос, Кошки представили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
вообще.