Когда вы какое-то время работаете на Scala, вы начинаете искать монады повсюду: сначала Option, потом «Или», «Попробовать», «IO» и тому подобное… но почему Validated не входит в их число? В этом посте я подробно рассмотрю некоторые очень интересные детали реализации Validated.

Примечание: первая часть этого поста — эта, Scala: What is Cat’s Validated?

Если мы углубимся в проверенную реализацию,

sealed abstract class Validated[+E, +A] extends Product with Serializable

Идя дальше, мы видим, что Product является базовым трейтом для работы с кортежами, а Serializable предоставляет методы для сериализации.

trait Product extends Any with Equals {
  /** The n^th^ element of this product, 0-based.  In other words, for a
   *  product `A(x,,1,,, ..., x,,k,,)`, returns `x,,(n+1),,` where `0 <= n < k`.
 
  def productElement(n: Int): Any

  def productArity: Int

  def productIterator: Iterator[Any] = ...
  }

Крутая вещь в Scala заключается в том, что кортежи могут быть не только значениями, но и функциями, поскольку они являются гражданами первого класса. И когда у вас есть кортежи функций, есть этот невероятный метод mapN, который можно использовать таким образом.

(validateUserName(username),
  validateAge(age)).mapN(RegistrationData)

Он параллельно оценивает каждый элемент кортежа, собирает частичные результаты и передает их функции в аргументе (в данном примере RegistrationData).

Ключевым моментом здесь является понятие «параллельно». Если бы Validated была монадой, то комбинация частичных проверок выполнялась бы с использованием flatMap ( или для понимания, что является синтаксическим сахаром), и они оценивались бы последовательно. Таким образом, в случае сбоя элемента процесс замыкается, и возвращаются только результаты первого сбоя. Используя mapN, мы получаем результаты всех оценок одновременно.

Возвращаясь к mapN, способ сбора зависит от типа элементов в кортеже. В случае проверки, если какой-либо из них дает сбой, результатом является сбой, объединяющий все частичные сбои (в этом случае NonEmptyList из Strings, содержащих ошибки), и если все успешно, функция получает частичные результаты. Откуда Scala это знает?

def mapN[Z](f: (A0, A1) => Z)(implicit functor: Functor[F], semigroupal: Semigroupal[F]): F[Z] = Semigroupal.map2(t2._1, t2._2)(f)

Это требует двух имплицитов. полугруппа, это что-то, что обеспечивает операцию «объединить» (как мы будем соединять элементы кортежа вместе) и функтор, это что-то который предоставляет нам карту для доступа к ValidationResult. Это довольно абстрактно и, вероятно, не очень ясно с моим объяснением, но мы можем использовать IntelliJ, чтобы увидеть используемые нами имплициты и результирующие типы, и вот результат.

Шаг за шагом,

  • кортеж (validateUserName(username), validateAge(age)) определяет функцию, которая получает String и Int и возвращает Z (который будет другим String и Int, но компилятор не знает пока что)
  • функтор, предоставленный cats.implicits._, предоставляет функцию карты для входа в результирующий ValidationResult и получения String и Int и передать их в RegistrationData.
  • полугруппа, также предоставляемая cats.implicits._, предоставляет функцию объединения, используемую mapN для объединения результатов validateUserName(username) и validateAge(age) . Он работает так, как ожидалось: если оба верны, возвращаются значения, а если одно или оба неверны, объединяются сообщения об ошибках и возвращаются.

Это довольно сложное объяснение, по крайней мере, для моих ограниченных знаний о Scala, поэтому я надеюсь, что оно помогло прояснить тему и дать некоторое представление об этой действительно полезной библиотеке. Однако я, вероятно, сказал что-то неправильно или плохо объяснил, поэтому любые отзывы более чем приветствуются.