diff --git a/checklist/src/main/scala/checklist/Indexable.scala b/checklist/src/main/scala/checklist/Indexable.scala index a20ff01..48842b4 100644 --- a/checklist/src/main/scala/checklist/Indexable.scala +++ b/checklist/src/main/scala/checklist/Indexable.scala @@ -1,30 +1,47 @@ package checklist -import scala.language.higherKinds trait Indexable[S[_]] { - def zipWithIndex[A](values: S[A]): S[(A, Int)] + def mapWithIndex[A, B](values: S[A])(f: (A, Int) => B): S[B] + def zipWithIndex[A](values: S[A]): S[(A, Int)] = mapWithIndex(values)((a, i) => (a, i)) } -object Indexable { +object Indexable extends LowPriorityIndexable { def apply[S[_]](implicit indexable: Indexable[S]): Indexable[S] = indexable implicit val listIndexable: Indexable[List] = new Indexable[List] { - def zipWithIndex[A](values: List[A]): List[(A, Int)] = + def mapWithIndex[A, B](values: List[A])(f: (A, Int) => B) = values.zipWithIndex.map { case (a, i) => f(a, i) } + override def zipWithIndex[A](values: List[A]): List[(A, Int)] = values.zipWithIndex } implicit val vectorIndexable: Indexable[Vector] = new Indexable[Vector] { - def zipWithIndex[A](values: Vector[A]): Vector[(A, Int)] = + def mapWithIndex[A, B](values: Vector[A])(f: (A, Int) => B) = values.zipWithIndex.map { case (a, i) => f(a, i) } + override def zipWithIndex[A](values: Vector[A]): Vector[(A, Int)] = values.zipWithIndex } implicit val streamIndexable: Indexable[Stream] = new Indexable[Stream] { - def zipWithIndex[A](values: Stream[A]): Stream[(A, Int)] = + def mapWithIndex[A, B](values: Stream[A])(f: (A, Int) => B) = values.zipWithIndex.map { case (a, i) => f(a, i) } + override def zipWithIndex[A](values: Stream[A]): Stream[(A, Int)] = values.zipWithIndex } -} \ No newline at end of file +} + +trait LowPriorityIndexable { + import cats.Traverse + // Most of the stuff below is stolen from cats 1.0.0-MF's Traverse. Once this lib is on cats 1.0.0, Indexable can disappear because traverse does all of this. + implicit def indexableFromTraverse[S[_]: Traverse]: Indexable[S] = { + new Indexable[S] { + import cats.data.State + import cats.implicits._ + def mapWithIndex[A, B](values: S[A])(f: (A, Int) => B): S[B] = + values.traverse(a => State((s: Int) => (s + 1, f(a, s)))).runA(0).value + } + } + +} diff --git a/checklist/src/main/scala/checklist/IndexableSyntax.scala b/checklist/src/main/scala/checklist/IndexableSyntax.scala new file mode 100644 index 0000000..4067a5a --- /dev/null +++ b/checklist/src/main/scala/checklist/IndexableSyntax.scala @@ -0,0 +1,16 @@ +package checklist + +import cats.implicits._ +import cats.{Applicative, Traverse} + +object IndexableSyntax extends IndexableSyntax + +trait IndexableSyntax { + implicit class IndexableOps[S[_], A](sa: S[A])(implicit indexable: Indexable[S]) { + def zipWithIndex: S[(A, Int)] = indexable.zipWithIndex(sa) + def mapWithIndex[B](f: (A, Int) => B): S[B] = indexable.mapWithIndex(sa)(f) + + def traverseWithIndex[F[_]: Applicative, B](f: (A, Int) => F[B])(implicit traverse: Traverse[S]): F[S[B]] = + indexable.mapWithIndex(sa)(f).sequence + } +} diff --git a/checklist/src/main/scala/checklist/Rule.scala b/checklist/src/main/scala/checklist/Rule.scala index e1c2ecd..de7fbb8 100644 --- a/checklist/src/main/scala/checklist/Rule.scala +++ b/checklist/src/main/scala/checklist/Rule.scala @@ -8,6 +8,7 @@ import scala.language.higherKinds import scala.util.matching.Regex import Message.errors import cats.data.NonEmptyList +import checklist.IndexableSyntax._ sealed abstract class Rule[A, B] { def apply(value: A): Checked[B] @@ -63,7 +64,7 @@ sealed abstract class Rule[A, B] { } } - def seq[S[_]: Indexable: Traverse]: Rule[S[A], S[B]] = + def seq[S[_]: Traverse]: Rule[S[A], S[B]] = Rule.sequence(this) def opt: Rule[Option[A], Option[B]] = @@ -347,10 +348,9 @@ trait CollectionRules { case None => Ior.left(messages) } - def sequence[S[_] : Indexable : Traverse, A, B](rule: Rule[A, B]): Rule[S[A], S[B]] = + def sequence[S[_] : Traverse : Indexable, A, B](rule: Rule[A, B]): Rule[S[A], S[B]] = pure { values => - Indexable[S].zipWithIndex(values).traverse { - case (value, index) => + values.traverseWithIndex { (value, index) => rule.prefix(index).apply(value) } } diff --git a/checklist/src/main/scala/checklist/syntax.scala b/checklist/src/main/scala/checklist/syntax.scala index dbd1d2a..959a914 100644 --- a/checklist/src/main/scala/checklist/syntax.scala +++ b/checklist/src/main/scala/checklist/syntax.scala @@ -1,3 +1,3 @@ package checklist -object syntax extends Rule1Syntax with MessageSyntax with CheckedSyntax +object syntax extends Rule1Syntax with MessageSyntax with CheckedSyntax with IndexableSyntax diff --git a/checklist/src/test/scala/checklist/RuleSpec.scala b/checklist/src/test/scala/checklist/RuleSpec.scala index 8b9191a..32d2eb4 100644 --- a/checklist/src/test/scala/checklist/RuleSpec.scala +++ b/checklist/src/test/scala/checklist/RuleSpec.scala @@ -1,6 +1,7 @@ package checklist import cats.data.Ior +import cats.implicits._ import org.scalatest._ import monocle.macros.Lenses import Rule._ @@ -118,7 +119,7 @@ class PropertyRulesSpec extends FreeSpec with Matchers { } "lengthLt" in { - val rule = lengthLt[String](5, errors("fail")) + val rule = lengthLt(5, errors("fail")) rule("") should be(Ior.right("")) rule("abcd") should be(Ior.right("abcd"))