0

Let's say I have:

val m: Map[String, Int] = Map("one" -> 1, "five" -> 5, "six" -> 6, "nine" -> 9)

and I have two functions:

def isNotDivisibleByTwo(i: Int): ValidatedNec[String, Int] = Validated.condNec(i%2!=0, i, s"$i is divisible by 2.")

def isNotDivisibleByThree(i: Int): ValidatedNec[String, Int] = Validated.condNec(i%3!=0, i, s"$i is divisible by 3.")

I want a function that gives me:

def sanitize(m: Map[String, Int]):Map[String, Validated[NonEmptyList[String], Int]] = ???

i.e. It should return all the numbers that satisfy the said two functions, and a mapping of all the failing numbers and their associated faults.
e.g. For the given list m, I want to get:

val result = Map(
  "one" -> Valid(1),
  "five -> Valid(5),
  "nine" -> Invalid(NonEmptyList("9 is dividible by 3")),
  "six" -> Invalid(NonEmptyList("6 is dividible by 2", "6 is dividible by 3"))
)

This is what I currently have:

import cats.data._

val m: Map[String, Int] = Map("one" -> 1, "five" -> 5, "six" -> 6, "nine" -> 9)

def isNotDivisibleByTwo(i: Int): ValidatedNec[String, Unit] = Validated.condNec(i%2!=0, (), s"$i is divisible by 2.")

def isNotDivisibleByThree(i: Int): ValidatedNec[String, Unit] = Validated.condNec(i%3!=0, (), s"$i is divisible by 3.")


def sanitize(m: Map[String, Int]): Map[String, Validated[NonEmptyChain[String], Int]] = {

  m.mapValues{
    i =>
      isNotDivisibleByTwo(i).product(
        isNotDivisibleByThree(i)
      ).map(_ => i)
  }

}

But, I am not happy with the way I am "composing" the validations.

How can I do this in the most catsy way?

Maths noob
  • 1,684
  • 20
  • 42

1 Answers1

6

You were so close.
Remember that the correct way to combine multiple Validates is using the Applicative syntax.

import cats.data.{Validated, ValidatedNec}
import cats.syntax.apply._

type ErrorsOr[A] = ValidatedNec[String, A]

def isNotDivisibleByTwo(i: Int): ErrorsOr[Int] =
  Validated.condNec((i % 2) != 0, i, s"$i is divisible by 2.")

def isNotDivisibleByThree(i: Int): ErrorsOr[Int] =
  Validated.condNec((i % 3) != 0, i, s"$i is divisible by 3.")

val map: Map[String, Int] = Map("one" -> 1, "five" -> 5, "six" -> 6, "nine" -> 9)

def sanitize(m: Map[String, Int]): Map[String, ErrorsOr[Int]] =
  m.view.mapValues { i =>
    (
      isNotDivisibleByTwo(i),
      isNotDivisibleByThree(i)
    ).tupled.map(_ => i)
  }.toMap

sanitize(map)
// res: Map[String, ErrorsOr[Int]] = Map(
//   "one" -> Valid(1),
//   "five" -> Valid(5),
//   "six" -> Invalid(Append(Singleton("6 is divisible by 2."), Singleton("6 is divisible by 3."))),
//   "nine" -> Invalid(Singleton("9 is divisible by 3."))
// )

However, you may make the code even more general, to work with any number of validations. By using traverse.
(In this case, you do not need any syntax import).

import cats.data.NonEmptyList

val validations: NonEmptyList[Int => ErrorsOr[Int]] = NonEmptyList.of(isNotDivisibleByTwo, isNotDivisibleByThree)

def sanitize[K, V](map: Map[K, V])
                  (validations: NonEmptyList[V => ErrorsOr[V]]): Map[K, ErrorsOr[V]] =
  map.view.mapValues(i => validations.traverse(f => f(i)).map(_ => i)).toMap

sanitize(map)(validations)
// res: Map[String, ErrorsOr[Int]] = Map(
//   "one" -> Valid(1),
//   "five" -> Valid(5),
//   "six" -> Invalid(Append(Singleton("6 is divisible by 2."), Singleton("6 is divisible by 3."))),
//   "nine" -> Invalid(Singleton("9 is divisible by 3."))
// )

The reason why I use .view.mapValues(...).toMap is because on Scala 2.13 mapValues is deprecated.