0

Suppose I need to check if a given list of numbers starts with of one or more 1, one or more 2 and one or more 3. If the check fails I would like to get all errors accumulated, e.g.

val check: List[Int] => Either[String, Unit] = ???

check(Nil)           // error : "expected 1", "expected 2", "expected 3"
check(List(1, 1, 3)) // error : "expected 2"
check(List(1, 1, 4)) // errors: "expected 2", "expected 3"
check(List(3, 4, 5)) // errors: "expected 1", "expected 2"
check(List(0, 0, 0)) // errors: "expected 1", "expected 2", "expected 3"

In order to implement check I am writing functions one, two, and three of type List[Int] => Either[String, List[Int]]:

import cats._, cats.data._, cats.implicits._

def num(n: Int): List[Int] => Either[String, List[Int]] = _ match { 
  case x::xs => if (x == n) (xs dropWhile (_ == n)).asRight else s"expected $n".asLeft
  case _ => s"expected $n".asLeft
}

val one   = num(1)
val two   = num(2)
val three = num(3)

scala> one(Nil)
res70: Either[String,List[Int]] = Left(expected 1)

scala> one(List(1, 1, 1))
res71: Either[String,List[Int]] = Right(List())

scala> one(List(2, 1, 1, 1))
res72: Either[String,List[Int]] = Left(expected 1)

scala> one(List(2, 3, 1, 1, 1))
res73: Either[String,List[Int]] = Left(expected 1)

How to compose functions one, two, and three to build check ? I can use Validated and all other stuff of cats.

Michael
  • 41,026
  • 70
  • 193
  • 341

2 Answers2

2

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)))
}
SergGr
  • 23,570
  • 2
  • 30
  • 51
1

Sounds like what you want here is cats.Apply which gives you a bunch of arity functions that allow you to chain functors.

Apply[Either[String, Unit], ?].map3(one, two, three) {
  case (res1, res2, res3) => ...
}

I'm using the kind-projector plugin to not have to manually compute the out type, but unless you want to actually use composable things to derive a final functor and you instead want to apply "intermediary" steps applicative style, this should be the way.

flavian
  • 28,161
  • 11
  • 65
  • 105
  • Thank you for the answer but I did not get it. Could you implement `check` in terms of `one`, `two`, and `three` ? – Michael Apr 29 '17 at 16:18