Note: the following solution uses almost nothing from cats
. Probably it can be shortened by some cats
expert.
I think it is impossibile to achive your goal with num
defined as yours because it looses state: it looses how many items were dropped in case of success. And if we need to return rest of the list anyway, it seems easier to use (Option[String], List[Int])
as return type:
def num(n: Int): List[Int] => (Option[String], List[Int]) = _ match {
case x :: xs => if (x == n) (None, (xs dropWhile (_ == n))) else (Some(s"expected $n"), xs)
case empty: List[Int] => (Some(s"expected $n"), empty)
}
Now you can create something that composes check such as:
def composedCheck(list: List[Int], checks: List[(List[Int]) => (Option[String], List[Int])]): Either[List[String], List[Int]] = {
val allChecksRes = checks.foldLeft((List.empty[String], list))((acc, check) => {
val checkRes = check(acc._2)
// shorter syntax but slower
//val errors = checkRes._1.toList ++ acc._1
// longer but without that much allocation
val errors = if (checkRes._1.isDefined) checkRes._1.get :: acc._1 else acc._1
(errors, checkRes._2)
})
if (allChecksRes._1.isEmpty) list.asRight else allChecksRes._1.reverse.asLeft
}
I think that returning here Either[List[String], List[Int]]
is the most natural thing that let's you process errors further in any way you like but also preserves the data (original list) in case everything is fine.
And finally you can create your check
as something like
val one = num(1)
val two = num(2)
val three = num(3)
val check: List[Int] => Either[String, List[Int]] = l => composedCheck(l, List(one, two, three)).left.map(errors => errors.mkString(", "))
All the code as one piece:
import cats.implicits._
object CatsChecks extends App {
def num(n: Int): List[Int] => (Option[String], List[Int]) = _ match {
case x :: xs => if (x == n) (None, (xs dropWhile (_ == n))) else (Some(s"expected $n"), xs)
case empty: List[Int] => (Some(s"expected $n"), empty)
}
def composedCheck(list: List[Int], checks: List[(List[Int]) => (Option[String], List[Int])]): Either[List[String], List[Int]] = {
val allChecksRes = checks.foldLeft((List.empty[String], list))((acc, check) => {
val checkRes = check(acc._2)
// shorter syntax but slower
//val errors = checkRes._1.toList ++ acc._1
// longer but without that much allocation
val errors = if (checkRes._1.isDefined) checkRes._1.get :: acc._1 else acc._1
(errors, checkRes._2)
})
if (allChecksRes._1.isEmpty) list.asRight else allChecksRes._1.reverse.asLeft
}
val one = num(1)
val two = num(2)
val three = num(3)
val check: List[Int] => Either[String, List[Int]] = l => composedCheck(l, List(one, two, three)).left.map(errors => errors.mkString(", "))
println(check(Nil)) // error : "expected 1", "expected 2", "expected 3"
println(check(List(1, 1, 3))) // error : "expected 2"
println(check(List(1, 1, 4))) // errors: "expected 2", "expected 3"
println(check(List(3, 4, 5))) // errors: "expected 1", "expected 2"
println(check(List(0, 0, 0))) // errors: "expected 1", "expected 2", "expected 3"
println(check(List(1, 1, 2, 3, 3, 4)))
}