diff --git a/.gitignore b/.gitignore index 24e370caee..bebf6c3f5a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ TAGS .idea/* .idea_modules .DS_Store +.vscode .sbtrc *.sublime-project *.sublime-workspace diff --git a/build.sbt b/build.sbt index 6f734aca3a..19a1a1aea7 100644 --- a/build.sbt +++ b/build.sbt @@ -398,6 +398,9 @@ def mimaSettings(moduleName: String) = exclude[MissingClassProblem]( "cats.kernel.compat.scalaVersionMoreSpecific$suppressUnusedImportWarningForScalaVersionMoreSpecific" ) + ) ++ //abstract package private classes + Seq( + exclude[DirectMissingMethodProblem]("cats.data.AbstractNonEmptyInstances.this") ) } diff --git a/core/src/main/scala-2.12/cats/compat/Vector.scala b/core/src/main/scala-2.12/cats/compat/Vector.scala new file mode 100644 index 0000000000..5970fc959c --- /dev/null +++ b/core/src/main/scala-2.12/cats/compat/Vector.scala @@ -0,0 +1,6 @@ +package cats.compat + +private[cats] object Vector { + def zipWith[A, B, C](fa: Vector[A], fb: Vector[B])(f: (A, B) => C): Vector[C] = + (fa, fb).zipped.map(f) +} \ No newline at end of file diff --git a/core/src/main/scala-2.13+/cats/compat/Vector.scala b/core/src/main/scala-2.13+/cats/compat/Vector.scala new file mode 100644 index 0000000000..e3f0f5e223 --- /dev/null +++ b/core/src/main/scala-2.13+/cats/compat/Vector.scala @@ -0,0 +1,6 @@ +package cats.compat + +private[cats] object Vector { + def zipWith[A, B, C](fa: Vector[A], fb: Vector[B])(f: (A, B) => C): Vector[C] = + fa.lazyZip(fb).map(f) +} diff --git a/core/src/main/scala-2.13+/cats/data/NonEmptyLazyList.scala b/core/src/main/scala-2.13+/cats/data/NonEmptyLazyList.scala index 2cbed1312d..c4cd5de0e9 100644 --- a/core/src/main/scala-2.13+/cats/data/NonEmptyLazyList.scala +++ b/core/src/main/scala-2.13+/cats/data/NonEmptyLazyList.scala @@ -330,30 +330,32 @@ class NonEmptyLazyListOps[A](private val value: NonEmptyLazyList[A]) extends Any sealed abstract private[data] class NonEmptyLazyListInstances extends NonEmptyLazyListInstances1 { - implicit val catsDataInstancesForNonEmptyLazyList - : Bimonad[NonEmptyLazyList] with NonEmptyTraverse[NonEmptyLazyList] with SemigroupK[NonEmptyLazyList] = - new AbstractNonEmptyInstances[LazyList, NonEmptyLazyList] { - - def extract[A](fa: NonEmptyLazyList[A]): A = fa.head - - def nonEmptyTraverse[G[_]: Apply, A, B](fa: NonEmptyLazyList[A])(f: A => G[B]): G[NonEmptyLazyList[B]] = - Foldable[LazyList] - .reduceRightToOption[A, G[LazyList[B]]](fa.tail)(a => Apply[G].map(f(a))(LazyList.apply(_))) { (a, lglb) => - Apply[G].map2Eval(f(a), lglb)(_ +: _) - } - .map { - case None => Apply[G].map(f(fa.head))(h => create(LazyList(h))) - case Some(gtail) => Apply[G].map2(f(fa.head), gtail)((h, t) => create(LazyList(h) ++ t)) - } - .value - - def reduceLeftTo[A, B](fa: NonEmptyLazyList[A])(f: A => B)(g: (B, A) => B): B = fa.reduceLeftTo(f)(g) - - def reduceRightTo[A, B](fa: NonEmptyLazyList[A])(f: A => B)(g: (A, cats.Eval[B]) => cats.Eval[B]): cats.Eval[B] = - Eval.defer(fa.reduceRightTo(a => Eval.now(f(a))) { (a, b) => - Eval.defer(g(a, b)) - }) - } + implicit val catsDataInstancesForNonEmptyLazyList: Bimonad[NonEmptyLazyList] + with NonEmptyTraverse[NonEmptyLazyList] + with SemigroupK[NonEmptyLazyList] + with Align[NonEmptyLazyList] = + new AbstractNonEmptyInstances[LazyList, NonEmptyLazyList] { + + def extract[A](fa: NonEmptyLazyList[A]): A = fa.head + + def nonEmptyTraverse[G[_]: Apply, A, B](fa: NonEmptyLazyList[A])(f: A => G[B]): G[NonEmptyLazyList[B]] = + Foldable[LazyList] + .reduceRightToOption[A, G[LazyList[B]]](fa.tail)(a => Apply[G].map(f(a))(LazyList.apply(_))) { (a, lglb) => + Apply[G].map2Eval(f(a), lglb)(_ +: _) + } + .map { + case None => Apply[G].map(f(fa.head))(h => create(LazyList(h))) + case Some(gtail) => Apply[G].map2(f(fa.head), gtail)((h, t) => create(LazyList(h) ++ t)) + } + .value + + def reduceLeftTo[A, B](fa: NonEmptyLazyList[A])(f: A => B)(g: (B, A) => B): B = fa.reduceLeftTo(f)(g) + + def reduceRightTo[A, B](fa: NonEmptyLazyList[A])(f: A => B)(g: (A, cats.Eval[B]) => cats.Eval[B]): cats.Eval[B] = + Eval.defer(fa.reduceRightTo(a => Eval.now(f(a))) { (a, b) => + Eval.defer(g(a, b)) + }) + } implicit def catsDataOrderForNonEmptyLazyList[A: Order]: Order[NonEmptyLazyList[A]] = Order[LazyList[A]].asInstanceOf[Order[NonEmptyLazyList[A]]] diff --git a/core/src/main/scala-2.13+/cats/instances/lazyList.scala b/core/src/main/scala-2.13+/cats/instances/lazyList.scala index 1d55cb8c21..6409472774 100644 --- a/core/src/main/scala-2.13+/cats/instances/lazyList.scala +++ b/core/src/main/scala-2.13+/cats/instances/lazyList.scala @@ -2,13 +2,15 @@ package cats package instances import cats.kernel import cats.syntax.show._ +import cats.data.Ior import scala.annotation.tailrec trait LazyListInstances extends cats.kernel.instances.LazyListInstances { implicit val catsStdInstancesForLazyList - : Traverse[LazyList] with Alternative[LazyList] with Monad[LazyList] with CoflatMap[LazyList] = - new Traverse[LazyList] with Alternative[LazyList] with Monad[LazyList] with CoflatMap[LazyList] { + : Traverse[LazyList] with Alternative[LazyList] with Monad[LazyList] with CoflatMap[LazyList] with Align[LazyList] = + new Traverse[LazyList] with Alternative[LazyList] with Monad[LazyList] with CoflatMap[LazyList] + with Align[LazyList] { def empty[A]: LazyList[A] = LazyList.empty @@ -121,6 +123,27 @@ trait LazyListInstances extends cats.kernel.instances.LazyListInstances { override def collectFirstSome[A, B](fa: LazyList[A])(f: A => Option[B]): Option[B] = fa.collectFirst(Function.unlift(f)) + + def functor: Functor[LazyList] = this + + def align[A, B](fa: LazyList[A], fb: LazyList[B]): LazyList[Ior[A, B]] = + alignWith(fa, fb)(identity) + + override def alignWith[A, B, C](fa: LazyList[A], fb: LazyList[B])(f: Ior[A, B] => C): LazyList[C] = { + val iterA = fa.iterator + val iterB = fb.iterator + + var result: LazyList[C] = LazyList.empty + + while (iterA.hasNext || iterB.hasNext) { + val ior = + if (iterA.hasNext && iterB.hasNext) Ior.both(iterA.next(), iterB.next()) + else if (iterA.hasNext) Ior.left(iterA.next()) + else Ior.right(iterB.next()) + result = result :+ f(ior) + } + result + } } implicit def catsStdShowForLazyList[A: Show]: Show[LazyList[A]] = diff --git a/core/src/main/scala/cats/Align.scala b/core/src/main/scala/cats/Align.scala new file mode 100644 index 0000000000..a6b890c681 --- /dev/null +++ b/core/src/main/scala/cats/Align.scala @@ -0,0 +1,47 @@ +package cats + +import simulacrum.typeclass + +import cats.data.Ior + +/** + * `Align` supports zipping together structures with different shapes, + * holding the results from either or both structures in an `Ior`. + * + * Must obey the laws in cats.laws.AlignLaws + */ +@typeclass trait Align[F[_]] { + + def functor: Functor[F] + + /** + * Pairs elements of two structures along the union of their shapes, using `Ior` to hold the results. + * + * Align[List].align(List(1, 2), List(10, 11, 12)) = List(Ior.Both(1, 10), Ior.Both(2, 11), Ior.Right(12)) + */ + def align[A, B](fa: F[A], fb: F[B]): F[Ior[A, B]] + + /** + * Combines elements similarly to `align`, using the provided function to compute the results. + */ + def alignWith[A, B, C](fa: F[A], fb: F[B])(f: Ior[A, B] => C): F[C] = + functor.map(align(fa, fb))(f) + + /** + * Align two structures with the same element, combining results according to their semigroup instances. + */ + def alignCombine[A: Semigroup](fa1: F[A], fa2: F[A]): F[A] = + alignWith(fa1, fa2)(_.merge) + + /** + * Same as `align`, but forgets from the type that one of the two elements must be present. + */ + def padZip[A, B](fa: F[A], fb: F[B]): F[(Option[A], Option[B])] = + alignWith(fa, fb)(_.pad) + + /** + * Same as `alignWith`, but forgets from the type that one of the two elements must be present. + */ + def padZipWith[A, B, C](fa: F[A], fb: F[B])(f: (Option[A], Option[B]) => C): F[C] = + alignWith(fa, fb)(ior => Function.tupled(f)(ior.pad)) +} diff --git a/core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala b/core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala index 57a65ecf42..e343e64509 100644 --- a/core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala +++ b/core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala @@ -4,14 +4,17 @@ package data abstract private[data] class AbstractNonEmptyInstances[F[_], NonEmptyF[_]](implicit MF: Monad[F], CF: CoflatMap[F], TF: Traverse[F], - SF: SemigroupK[F]) + SF: SemigroupK[F], + AF: Align[F]) extends Bimonad[NonEmptyF] with NonEmptyTraverse[NonEmptyF] - with SemigroupK[NonEmptyF] { + with SemigroupK[NonEmptyF] + with Align[NonEmptyF] { val monadInstance = MF.asInstanceOf[Monad[NonEmptyF]] val coflatMapInstance = CF.asInstanceOf[CoflatMap[NonEmptyF]] val traverseInstance = Traverse[F].asInstanceOf[Traverse[NonEmptyF]] val semiGroupKInstance = SemigroupK[F].asInstanceOf[SemigroupK[NonEmptyF]] + val alignInstance = Align[F].asInstanceOf[Align[NonEmptyF]] def combineK[A](a: NonEmptyF[A], b: NonEmptyF[A]): NonEmptyF[A] = semiGroupKInstance.combineK(a, b) @@ -78,4 +81,11 @@ abstract private[data] class AbstractNonEmptyInstances[F[_], NonEmptyF[_]](impli override def collectFirstSome[A, B](fa: NonEmptyF[A])(f: A => Option[B]): Option[B] = traverseInstance.collectFirstSome(fa)(f) + def align[A, B](fa: NonEmptyF[A], fb: NonEmptyF[B]): NonEmptyF[Ior[A, B]] = + alignInstance.align(fa, fb) + + override def functor: Functor[NonEmptyF] = alignInstance.functor + + override def alignWith[A, B, C](fa: NonEmptyF[A], fb: NonEmptyF[B])(f: Ior[A, B] => C): NonEmptyF[C] = + alignInstance.alignWith(fa, fb)(f) } diff --git a/core/src/main/scala/cats/data/Chain.scala b/core/src/main/scala/cats/data/Chain.scala index 7a9cacf0c0..05a90cbc36 100644 --- a/core/src/main/scala/cats/data/Chain.scala +++ b/core/src/main/scala/cats/data/Chain.scala @@ -681,8 +681,8 @@ sealed abstract private[data] class ChainInstances extends ChainInstances1 { } implicit val catsDataInstancesForChain - : Traverse[Chain] with Alternative[Chain] with Monad[Chain] with CoflatMap[Chain] = - new Traverse[Chain] with Alternative[Chain] with Monad[Chain] with CoflatMap[Chain] { + : Traverse[Chain] with Alternative[Chain] with Monad[Chain] with CoflatMap[Chain] with Align[Chain] = + new Traverse[Chain] with Alternative[Chain] with Monad[Chain] with CoflatMap[Chain] with Align[Chain] { def foldLeft[A, B](fa: Chain[A], b: B)(f: (B, A) => B): B = fa.foldLeft(b)(f) def foldRight[A, B](fa: Chain[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = @@ -743,6 +743,27 @@ sealed abstract private[data] class ChainInstances extends ChainInstances1 { } override def get[A](fa: Chain[A])(idx: Long): Option[A] = fa.get(idx) + + def functor: Functor[Chain] = this + + def align[A, B](fa: Chain[A], fb: Chain[B]): Chain[Ior[A, B]] = + alignWith(fa, fb)(identity) + + override def alignWith[A, B, C](fa: Chain[A], fb: Chain[B])(f: Ior[A, B] => C): Chain[C] = { + val iterA = fa.iterator + val iterB = fb.iterator + + var result: Chain[C] = Chain.empty + + while (iterA.hasNext || iterB.hasNext) { + val ior = + if (iterA.hasNext && iterB.hasNext) Ior.both(iterA.next(), iterB.next()) + else if (iterA.hasNext) Ior.left(iterA.next()) + else Ior.right(iterB.next()) + result = result :+ f(ior) + } + result + } } implicit def catsDataShowForChain[A](implicit A: Show[A]): Show[Chain[A]] = diff --git a/core/src/main/scala/cats/data/NonEmptyChain.scala b/core/src/main/scala/cats/data/NonEmptyChain.scala index fc4ed37435..a5a736b6f3 100644 --- a/core/src/main/scala/cats/data/NonEmptyChain.scala +++ b/core/src/main/scala/cats/data/NonEmptyChain.scala @@ -418,40 +418,42 @@ class NonEmptyChainOps[A](private val value: NonEmptyChain[A]) extends AnyVal { sealed abstract private[data] class NonEmptyChainInstances extends NonEmptyChainInstances1 { - implicit val catsDataInstancesForNonEmptyChain - : SemigroupK[NonEmptyChain] with NonEmptyTraverse[NonEmptyChain] with Bimonad[NonEmptyChain] = - new AbstractNonEmptyInstances[Chain, NonEmptyChain] { - def extract[A](fa: NonEmptyChain[A]): A = fa.head - - def nonEmptyTraverse[G[_]: Apply, A, B](fa: NonEmptyChain[A])(f: A => G[B]): G[NonEmptyChain[B]] = - Foldable[Chain] - .reduceRightToOption[A, G[Chain[B]]](fa.tail)(a => Apply[G].map(f(a))(Chain.one)) { (a, lglb) => - Apply[G].map2Eval(f(a), lglb)(_ +: _) - } - .map { - case None => Apply[G].map(f(fa.head))(NonEmptyChain.one) - case Some(gtail) => Apply[G].map2(f(fa.head), gtail)((h, t) => create(Chain.one(h) ++ t)) - } - .value - - override def size[A](fa: NonEmptyChain[A]): Long = fa.length - - override def reduceLeft[A](fa: NonEmptyChain[A])(f: (A, A) => A): A = - fa.reduceLeft(f) - - override def reduce[A](fa: NonEmptyChain[A])(implicit A: Semigroup[A]): A = - fa.reduce - - def reduceLeftTo[A, B](fa: NonEmptyChain[A])(f: A => B)(g: (B, A) => B): B = fa.reduceLeftTo(f)(g) - - def reduceRightTo[A, B](fa: NonEmptyChain[A])(f: A => B)(g: (A, cats.Eval[B]) => cats.Eval[B]): cats.Eval[B] = - Eval.defer(fa.reduceRightTo(a => Eval.now(f(a))) { (a, b) => - Eval.defer(g(a, b)) - }) - - override def get[A](fa: NonEmptyChain[A])(idx: Long): Option[A] = - if (idx == 0) Some(fa.head) else fa.tail.get(idx - 1) - } + implicit val catsDataInstancesForNonEmptyChain: SemigroupK[NonEmptyChain] + with NonEmptyTraverse[NonEmptyChain] + with Bimonad[NonEmptyChain] + with Align[NonEmptyChain] = + new AbstractNonEmptyInstances[Chain, NonEmptyChain] { + def extract[A](fa: NonEmptyChain[A]): A = fa.head + + def nonEmptyTraverse[G[_]: Apply, A, B](fa: NonEmptyChain[A])(f: A => G[B]): G[NonEmptyChain[B]] = + Foldable[Chain] + .reduceRightToOption[A, G[Chain[B]]](fa.tail)(a => Apply[G].map(f(a))(Chain.one)) { (a, lglb) => + Apply[G].map2Eval(f(a), lglb)(_ +: _) + } + .map { + case None => Apply[G].map(f(fa.head))(NonEmptyChain.one) + case Some(gtail) => Apply[G].map2(f(fa.head), gtail)((h, t) => create(Chain.one(h) ++ t)) + } + .value + + override def size[A](fa: NonEmptyChain[A]): Long = fa.length + + override def reduceLeft[A](fa: NonEmptyChain[A])(f: (A, A) => A): A = + fa.reduceLeft(f) + + override def reduce[A](fa: NonEmptyChain[A])(implicit A: Semigroup[A]): A = + fa.reduce + + def reduceLeftTo[A, B](fa: NonEmptyChain[A])(f: A => B)(g: (B, A) => B): B = fa.reduceLeftTo(f)(g) + + def reduceRightTo[A, B](fa: NonEmptyChain[A])(f: A => B)(g: (A, cats.Eval[B]) => cats.Eval[B]): cats.Eval[B] = + Eval.defer(fa.reduceRightTo(a => Eval.now(f(a))) { (a, b) => + Eval.defer(g(a, b)) + }) + + override def get[A](fa: NonEmptyChain[A])(idx: Long): Option[A] = + if (idx == 0) Some(fa.head) else fa.tail.get(idx - 1) + } implicit def catsDataOrderForNonEmptyChain[A: Order]: Order[NonEmptyChain[A]] = Order[Chain[A]].asInstanceOf[Order[NonEmptyChain[A]]] diff --git a/core/src/main/scala/cats/data/NonEmptyList.scala b/core/src/main/scala/cats/data/NonEmptyList.scala index 2e09836c65..fe1f58758b 100644 --- a/core/src/main/scala/cats/data/NonEmptyList.scala +++ b/core/src/main/scala/cats/data/NonEmptyList.scala @@ -510,9 +510,9 @@ object NonEmptyList extends NonEmptyListInstances { sealed abstract private[data] class NonEmptyListInstances extends NonEmptyListInstances0 { implicit val catsDataInstancesForNonEmptyList - : SemigroupK[NonEmptyList] with Bimonad[NonEmptyList] with NonEmptyTraverse[NonEmptyList] = + : SemigroupK[NonEmptyList] with Bimonad[NonEmptyList] with NonEmptyTraverse[NonEmptyList] with Align[NonEmptyList] = new NonEmptyReducible[NonEmptyList, List] with SemigroupK[NonEmptyList] with Bimonad[NonEmptyList] - with NonEmptyTraverse[NonEmptyList] { + with NonEmptyTraverse[NonEmptyList] with Align[NonEmptyList] { def combineK[A](a: NonEmptyList[A], b: NonEmptyList[A]): NonEmptyList[A] = a.concatNel(b) @@ -617,6 +617,25 @@ sealed abstract private[data] class NonEmptyListInstances extends NonEmptyListIn override def get[A](fa: NonEmptyList[A])(idx: Long): Option[A] = if (idx == 0) Some(fa.head) else Foldable[List].get(fa.tail)(idx - 1) + + def functor: Functor[NonEmptyList] = this + + def align[A, B](fa: NonEmptyList[A], fb: NonEmptyList[B]): NonEmptyList[Ior[A, B]] = + alignWith(fa, fb)(identity) + + override def alignWith[A, B, C](fa: NonEmptyList[A], fb: NonEmptyList[B])(f: Ior[A, B] => C): NonEmptyList[C] = { + + @tailrec + def go(as: List[A], bs: List[B], acc: List[C]): List[C] = (as, bs) match { + case (Nil, Nil) => acc + case (Nil, y :: ys) => go(Nil, ys, f(Ior.right(y)) :: acc) + case (x :: xs, Nil) => go(xs, Nil, f(Ior.left(x)) :: acc) + case (x :: xs, y :: ys) => go(xs, ys, f(Ior.both(x, y)) :: acc) + } + + NonEmptyList(f(Ior.both(fa.head, fb.head)), go(fa.tail, fb.tail, Nil).reverse) + } + } implicit def catsDataShowForNonEmptyList[A](implicit A: Show[A]): Show[NonEmptyList[A]] = diff --git a/core/src/main/scala/cats/data/NonEmptyMapImpl.scala b/core/src/main/scala/cats/data/NonEmptyMapImpl.scala index 3195c88694..abd52b4aa4 100644 --- a/core/src/main/scala/cats/data/NonEmptyMapImpl.scala +++ b/core/src/main/scala/cats/data/NonEmptyMapImpl.scala @@ -268,8 +268,8 @@ sealed class NonEmptyMapOps[K, A](val value: NonEmptyMap[K, A]) { sealed abstract private[data] class NonEmptyMapInstances extends NonEmptyMapInstances0 { implicit def catsDataInstancesForNonEmptyMap[K: Order] - : SemigroupK[NonEmptyMap[K, *]] with NonEmptyTraverse[NonEmptyMap[K, *]] = - new SemigroupK[NonEmptyMap[K, *]] with NonEmptyTraverse[NonEmptyMap[K, *]] { + : SemigroupK[NonEmptyMap[K, *]] with NonEmptyTraverse[NonEmptyMap[K, *]] with Align[NonEmptyMap[K, *]] = + new SemigroupK[NonEmptyMap[K, *]] with NonEmptyTraverse[NonEmptyMap[K, *]] with Align[NonEmptyMap[K, *]] { override def map[A, B](fa: NonEmptyMap[K, A])(f: A => B): NonEmptyMap[K, B] = fa.map(f) @@ -316,6 +316,11 @@ sealed abstract private[data] class NonEmptyMapInstances extends NonEmptyMapInst override def toNonEmptyList[A](fa: NonEmptyMap[K, A]): NonEmptyList[A] = NonEmptyList(fa.head._2, fa.tail.toList.map(_._2)) + + def functor: Functor[NonEmptyMap[K, *]] = this + + def align[A, B](fa: NonEmptyMap[K, A], fb: NonEmptyMap[K, B]): NonEmptyMap[K, Ior[A, B]] = + NonEmptyMap.fromMapUnsafe(Align[SortedMap[K, *]].align(fa.toSortedMap, fb.toSortedMap)) } implicit def catsDataHashForNonEmptyMap[K: Hash: Order, A: Hash]: Hash[NonEmptyMap[K, A]] = diff --git a/core/src/main/scala/cats/data/NonEmptyVector.scala b/core/src/main/scala/cats/data/NonEmptyVector.scala index 1121668b0f..9593054c82 100644 --- a/core/src/main/scala/cats/data/NonEmptyVector.scala +++ b/core/src/main/scala/cats/data/NonEmptyVector.scala @@ -238,10 +238,12 @@ final class NonEmptyVector[+A] private (val toVector: Vector[A]) extends AnyVal @suppressUnusedImportWarningForScalaVersionSpecific sealed abstract private[data] class NonEmptyVectorInstances { - implicit val catsDataInstancesForNonEmptyVector - : SemigroupK[NonEmptyVector] with Bimonad[NonEmptyVector] with NonEmptyTraverse[NonEmptyVector] = + implicit val catsDataInstancesForNonEmptyVector: SemigroupK[NonEmptyVector] + with Bimonad[NonEmptyVector] + with NonEmptyTraverse[NonEmptyVector] + with Align[NonEmptyVector] = new NonEmptyReducible[NonEmptyVector, Vector] with SemigroupK[NonEmptyVector] with Bimonad[NonEmptyVector] - with NonEmptyTraverse[NonEmptyVector] { + with NonEmptyTraverse[NonEmptyVector] with Align[NonEmptyVector] { def combineK[A](a: NonEmptyVector[A], b: NonEmptyVector[A]): NonEmptyVector[A] = a.concatNev(b) @@ -358,6 +360,15 @@ sealed abstract private[data] class NonEmptyVectorInstances { override def toNonEmptyList[A](fa: NonEmptyVector[A]): NonEmptyList[A] = NonEmptyList(fa.head, fa.tail.toList) + + def functor: Functor[NonEmptyVector] = this + + def align[A, B](fa: NonEmptyVector[A], fb: NonEmptyVector[B]): NonEmptyVector[Ior[A, B]] = + NonEmptyVector.fromVectorUnsafe(Align[Vector].align(fa.toVector, fb.toVector)) + + override def alignWith[A, B, C](fa: NonEmptyVector[A], + fb: NonEmptyVector[B])(f: Ior[A, B] => C): NonEmptyVector[C] = + NonEmptyVector.fromVectorUnsafe(Align[Vector].alignWith(fa.toVector, fb.toVector)(f)) } implicit def catsDataEqForNonEmptyVector[A](implicit A: Eq[A]): Eq[NonEmptyVector[A]] = diff --git a/core/src/main/scala/cats/instances/either.scala b/core/src/main/scala/cats/instances/either.scala index 572b5df3c8..6d62d95535 100644 --- a/core/src/main/scala/cats/instances/either.scala +++ b/core/src/main/scala/cats/instances/either.scala @@ -4,6 +4,7 @@ package instances import cats.syntax.EitherUtil import cats.syntax.either._ import scala.annotation.tailrec +import cats.data.Ior trait EitherInstances extends cats.kernel.instances.EitherInstances { implicit val catsStdBitraverseForEither: Bitraverse[Either] = @@ -30,8 +31,9 @@ trait EitherInstances extends cats.kernel.instances.EitherInstances { } // scalastyle:off method.length - implicit def catsStdInstancesForEither[A]: MonadError[Either[A, *], A] with Traverse[Either[A, *]] = - new MonadError[Either[A, *], A] with Traverse[Either[A, *]] { + implicit def catsStdInstancesForEither[A] + : MonadError[Either[A, *], A] with Traverse[Either[A, *]] with Align[Either[A, *]] = + new MonadError[Either[A, *], A] with Traverse[Either[A, *]] with Align[Either[A, *]] { def pure[B](b: B): Either[A, B] = Right(b) def flatMap[B, C](fa: Either[A, B])(f: B => Either[A, C]): Either[A, C] = @@ -139,6 +141,25 @@ trait EitherInstances extends cats.kernel.instances.EitherInstances { override def isEmpty[B](fab: Either[A, B]): Boolean = fab.isLeft + + def functor: Functor[Either[A, *]] = this + + def align[B, C](fa: Either[A, B], fb: Either[A, C]): Either[A, Ior[B, C]] = + alignWith(fa, fb)(identity) + + override def alignWith[B, C, D](fb: Either[A, B], fc: Either[A, C])(f: Ior[B, C] => D): Either[A, D] = fb match { + case left @ Left(a) => + fc match { + case Left(_) => left.rightCast[D] + case Right(c) => Right(f(Ior.right(c))) + } + case Right(b) => + fc match { + case Left(a) => Right(f(Ior.left(b))) + case Right(c) => Right(f(Ior.both(b, c))) + } + } + } // scalastyle:on method.length diff --git a/core/src/main/scala/cats/instances/list.scala b/core/src/main/scala/cats/instances/list.scala index 975fa14519..d7bbd0319e 100644 --- a/core/src/main/scala/cats/instances/list.scala +++ b/core/src/main/scala/cats/instances/list.scala @@ -6,10 +6,13 @@ import cats.syntax.show._ import scala.annotation.tailrec import scala.collection.mutable.ListBuffer +import cats.data.Ior + trait ListInstances extends cats.kernel.instances.ListInstances { - implicit val catsStdInstancesForList: Traverse[List] with Alternative[List] with Monad[List] with CoflatMap[List] = - new Traverse[List] with Alternative[List] with Monad[List] with CoflatMap[List] { + implicit val catsStdInstancesForList + : Traverse[List] with Alternative[List] with Monad[List] with CoflatMap[List] with Align[List] = + new Traverse[List] with Alternative[List] with Monad[List] with CoflatMap[List] with Align[List] { def empty[A]: List[A] = Nil def combineK[A](x: List[A], y: List[A]): List[A] = x ++ y @@ -74,6 +77,22 @@ trait ListInstances extends cats.kernel.instances.ListInstances { G.map2Eval(f(a), lglb)(_ :: _) }.value + def functor: Functor[List] = this + + def align[A, B](fa: List[A], fb: List[B]): List[A Ior B] = + alignWith(fa, fb)(identity) + + override def alignWith[A, B, C](fa: List[A], fb: List[B])(f: Ior[A, B] => C): List[C] = { + @tailrec def loop(buf: ListBuffer[C], as: List[A], bs: List[B]): List[C] = + (as, bs) match { + case (a :: atail, b :: btail) => loop(buf += f(Ior.Both(a, b)), atail, btail) + case (Nil, Nil) => buf.toList + case (arest, Nil) => (buf ++= arest.map(a => f(Ior.left(a)))).toList + case (Nil, brest) => (buf ++= brest.map(b => f(Ior.right(b)))).toList + } + loop(ListBuffer.empty[C], fa, fb) + } + override def mapWithIndex[A, B](fa: List[A])(f: (A, Int) => B): List[B] = fa.iterator.zipWithIndex.map(ai => f(ai._1, ai._2)).toList @@ -142,7 +161,6 @@ trait ListInstances extends cats.kernel.instances.ListInstances { override def collectFirstSome[A, B](fa: List[A])(f: A => Option[B]): Option[B] = fa.collectFirst(Function.unlift(f)) - } implicit def catsStdShowForList[A: Show]: Show[List[A]] = diff --git a/core/src/main/scala/cats/instances/map.scala b/core/src/main/scala/cats/instances/map.scala index a0524a88b0..4cfacff041 100644 --- a/core/src/main/scala/cats/instances/map.scala +++ b/core/src/main/scala/cats/instances/map.scala @@ -6,6 +6,8 @@ import cats.kernel.CommutativeMonoid import scala.annotation.tailrec import cats.arrow.Compose +import cats.data.Ior + trait MapInstances extends cats.kernel.instances.MapInstances { implicit def catsStdShowForMap[A, B](implicit showA: Show[A], showB: Show[B]): Show[Map[A, B]] = @@ -17,8 +19,8 @@ trait MapInstances extends cats.kernel.instances.MapInstances { } // scalastyle:off method.length - implicit def catsStdInstancesForMap[K]: UnorderedTraverse[Map[K, *]] with FlatMap[Map[K, *]] = - new UnorderedTraverse[Map[K, *]] with FlatMap[Map[K, *]] { + implicit def catsStdInstancesForMap[K]: UnorderedTraverse[Map[K, *]] with FlatMap[Map[K, *]] with Align[Map[K, *]] = + new UnorderedTraverse[Map[K, *]] with FlatMap[Map[K, *]] with Align[Map[K, *]] { def unorderedTraverse[G[_], A, B]( fa: Map[K, A] @@ -88,6 +90,26 @@ trait MapInstances extends cats.kernel.instances.MapInstances { override def exists[A](fa: Map[K, A])(p: A => Boolean): Boolean = fa.exists(pair => p(pair._2)) + def functor: Functor[Map[K, *]] = this + + def align[A, B](fa: Map[K, A], fb: Map[K, B]): Map[K, A Ior B] = + alignWith(fa, fb)(identity) + + override def alignWith[A, B, C](fa: Map[K, A], fb: Map[K, B])(f: Ior[A, B] => C): Map[K, C] = { + val keys = fa.keySet ++ fb.keySet + val builder = Map.newBuilder[K, C] + builder.sizeHint(keys.size) + keys + .foldLeft(builder) { (builder, k) => + (fa.get(k), fb.get(k)) match { + case (Some(a), Some(b)) => builder += k -> f(Ior.both(a, b)) + case (Some(a), None) => builder += k -> f(Ior.left(a)) + case (None, Some(b)) => builder += k -> f(Ior.right(b)) + case (None, None) => ??? // should not happen + } + } + .result() + } } // scalastyle:on method.length diff --git a/core/src/main/scala/cats/instances/option.scala b/core/src/main/scala/cats/instances/option.scala index 5c2ae4b8f0..11250ee95a 100644 --- a/core/src/main/scala/cats/instances/option.scala +++ b/core/src/main/scala/cats/instances/option.scala @@ -2,6 +2,7 @@ package cats package instances import scala.annotation.tailrec +import cats.data.Ior trait OptionInstances extends cats.kernel.instances.OptionInstances { @@ -9,9 +10,10 @@ trait OptionInstances extends cats.kernel.instances.OptionInstances { with MonadError[Option, Unit] with Alternative[Option] with CommutativeMonad[Option] - with CoflatMap[Option] = + with CoflatMap[Option] + with Align[Option] = new Traverse[Option] with MonadError[Option, Unit] with Alternative[Option] with CommutativeMonad[Option] - with CoflatMap[Option] { + with CoflatMap[Option] with Align[Option] { def empty[A]: Option[A] = None @@ -116,6 +118,19 @@ trait OptionInstances extends cats.kernel.instances.OptionInstances { override def collectFirst[A, B](fa: Option[A])(pf: PartialFunction[A, B]): Option[B] = fa.collectFirst(pf) override def collectFirstSome[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = fa.flatMap(f) + + def functor: Functor[Option] = this + + def align[A, B](fa: Option[A], fb: Option[B]): Option[A Ior B] = + alignWith(fa, fb)(identity) + + override def alignWith[A, B, C](fa: Option[A], fb: Option[B])(f: Ior[A, B] => C): Option[C] = + (fa, fb) match { + case (None, None) => None + case (Some(a), None) => Some(f(Ior.left(a))) + case (None, Some(b)) => Some(f(Ior.right(b))) + case (Some(a), Some(b)) => Some(f(Ior.both(a, b))) + } } implicit def catsStdShowForOption[A](implicit A: Show[A]): Show[Option[A]] = diff --git a/core/src/main/scala/cats/instances/sortedMap.scala b/core/src/main/scala/cats/instances/sortedMap.scala index 8ed33c47d7..aa10b8d4d8 100644 --- a/core/src/main/scala/cats/instances/sortedMap.scala +++ b/core/src/main/scala/cats/instances/sortedMap.scala @@ -5,6 +5,9 @@ import cats.kernel._ import scala.annotation.tailrec import scala.collection.immutable.SortedMap +import cats.Align +import cats.Functor +import cats.data.Ior trait SortedMapInstances extends SortedMapInstances2 { @@ -25,8 +28,9 @@ trait SortedMapInstances extends SortedMapInstances2 { } // scalastyle:off method.length - implicit def catsStdInstancesForSortedMap[K: Order]: Traverse[SortedMap[K, *]] with FlatMap[SortedMap[K, *]] = - new Traverse[SortedMap[K, *]] with FlatMap[SortedMap[K, *]] { + implicit def catsStdInstancesForSortedMap[K: Order] + : Traverse[SortedMap[K, *]] with FlatMap[SortedMap[K, *]] with Align[SortedMap[K, *]] = + new Traverse[SortedMap[K, *]] with FlatMap[SortedMap[K, *]] with Align[SortedMap[K, *]] { implicit val orderingK: Ordering[K] = Order[K].toOrdering @@ -109,6 +113,27 @@ trait SortedMapInstances extends SortedMapInstances2 { override def collectFirstSome[A, B](fa: SortedMap[K, A])(f: A => Option[B]): Option[B] = collectFirst(fa)(Function.unlift(f)) + + def functor: Functor[SortedMap[K, *]] = this + + def align[A, B](fa: SortedMap[K, A], fb: SortedMap[K, B]): SortedMap[K, Ior[A, B]] = + alignWith(fa, fb)(identity) + + override def alignWith[A, B, C](fa: SortedMap[K, A], fb: SortedMap[K, B])(f: Ior[A, B] => C): SortedMap[K, C] = { + val keys = fa.keySet ++ fb.keySet + val builder = SortedMap.newBuilder[K, C] + builder.sizeHint(keys.size) + keys + .foldLeft(builder) { (builder, k) => + (fa.get(k), fb.get(k)) match { + case (Some(a), Some(b)) => builder += k -> f(Ior.both(a, b)) + case (Some(a), None) => builder += k -> f(Ior.left(a)) + case (None, Some(b)) => builder += k -> f(Ior.right(b)) + case (None, None) => ??? // should not happen + } + } + .result() + } } } diff --git a/core/src/main/scala/cats/instances/vector.scala b/core/src/main/scala/cats/instances/vector.scala index 5c27e6f4b9..d87f5108ad 100644 --- a/core/src/main/scala/cats/instances/vector.scala +++ b/core/src/main/scala/cats/instances/vector.scala @@ -6,11 +6,12 @@ import cats.syntax.show._ import scala.annotation.tailrec import scala.collection.+: import scala.collection.immutable.VectorBuilder +import cats.data.Ior trait VectorInstances extends cats.kernel.instances.VectorInstances { implicit val catsStdInstancesForVector - : Traverse[Vector] with Monad[Vector] with Alternative[Vector] with CoflatMap[Vector] = - new Traverse[Vector] with Monad[Vector] with Alternative[Vector] with CoflatMap[Vector] { + : Traverse[Vector] with Monad[Vector] with Alternative[Vector] with CoflatMap[Vector] with Align[Vector] = + new Traverse[Vector] with Monad[Vector] with Alternative[Vector] with CoflatMap[Vector] with Align[Vector] { def empty[A]: Vector[A] = Vector.empty[A] @@ -109,6 +110,17 @@ trait VectorInstances extends cats.kernel.instances.VectorInstances { override def algebra[A]: Monoid[Vector[A]] = new kernel.instances.VectorMonoid[A] + def functor: Functor[Vector] = this + + def align[A, B](fa: Vector[A], fb: Vector[B]): Vector[A Ior B] = { + val aLarger = fa.size >= fb.size + if (aLarger) { + cats.compat.Vector.zipWith(fa, fb)(Ior.both) ++ fa.drop(fb.size).map(Ior.left) + } else { + cats.compat.Vector.zipWith(fa, fb)(Ior.both) ++ fb.drop(fa.size).map(Ior.right) + } + } + override def collectFirst[A, B](fa: Vector[A])(pf: PartialFunction[A, B]): Option[B] = fa.collectFirst(pf) override def collectFirstSome[A, B](fa: Vector[A])(f: A => Option[B]): Option[B] = diff --git a/core/src/main/scala/cats/syntax/align.scala b/core/src/main/scala/cats/syntax/align.scala new file mode 100644 index 0000000000..bd41650f42 --- /dev/null +++ b/core/src/main/scala/cats/syntax/align.scala @@ -0,0 +1,4 @@ +package cats +package syntax + +trait AlignSyntax extends Align.ToAlignOps diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index 2b19d210e9..e93083fe37 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -13,6 +13,7 @@ abstract class AllSyntaxBinCompat trait AllSyntax extends AlternativeSyntax + with AlignSyntax with ApplicativeSyntax with ApplicativeErrorSyntax with ApplySyntax diff --git a/core/src/main/scala/cats/syntax/package.scala b/core/src/main/scala/cats/syntax/package.scala index a6aa644a67..595573dce4 100644 --- a/core/src/main/scala/cats/syntax/package.scala +++ b/core/src/main/scala/cats/syntax/package.scala @@ -1,6 +1,7 @@ package cats package object syntax { + object align extends AlignSyntax object all extends AllSyntaxBinCompat object alternative extends AlternativeSyntax object applicative extends ApplicativeSyntax diff --git a/laws/src/main/scala/cats/laws/AlignLaws.scala b/laws/src/main/scala/cats/laws/AlignLaws.scala new file mode 100644 index 0000000000..b21fcd36b8 --- /dev/null +++ b/laws/src/main/scala/cats/laws/AlignLaws.scala @@ -0,0 +1,48 @@ +package cats +package laws + +import cats.syntax.align._ +import cats.syntax.functor._ + +import cats.data.Ior +import cats.data.Ior.{Left, Right, Both} + +/** + * Laws that must be obeyed by any `Align`. + */ +trait AlignLaws[F[_]] { + implicit def F: Align[F] + + implicit val functor: Functor[F] = F.functor + + def alignAssociativity[A, B, C](fa: F[A], fb: F[B], fc: F[C]): IsEq[F[Ior[Ior[A, B], C]]] = + fa.align(fb).align(fc) <-> fa.align(fb.align(fc)).map(assoc) + + def alignSelfBoth[A](fa: F[A]): IsEq[F[A Ior A]] = + fa.align(fa) <-> fa.map(a => Ior.both(a, a)) + + def alignHomomorphism[A, B, C, D](fa: F[A], fb: F[B], f: A => C, g: B => D): IsEq[F[C Ior D]] = + fa.map(f).align(fb.map(g)) <-> fa.align(fb).map(_.bimap(f, g)) + + def alignWithConsistent[A, B, C](fa: F[A], fb: F[B], f: A Ior B => C): IsEq[F[C]] = + fa.alignWith(fb)(f) <-> fa.align(fb).map(f) + + private def assoc[A, B, C](x: Ior[A, Ior[B, C]]): Ior[Ior[A, B], C] = x match { + case Left(a) => Left(Left(a)) + case Right(bc) => bc match { + case Left(b) => Left(Right(b)) + case Right(c) => Right(c) + case Both(b, c) => Both(Right(b), c) + } + case Both(a, bc) => bc match { + case Left(b) => Left(Both(a, b)) + case Right(c) => Both(Left(a), c) + case Both(b, c) => Both(Both(a, b), c) + } + } +} + +object AlignLaws { + def apply[F[_]](implicit ev: Align[F]): AlignLaws[F] = + new AlignLaws[F] { def F: Align[F] = ev } +} diff --git a/laws/src/main/scala/cats/laws/discipline/AlignTests.scala b/laws/src/main/scala/cats/laws/discipline/AlignTests.scala new file mode 100644 index 0000000000..932a3eb1cd --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/AlignTests.scala @@ -0,0 +1,47 @@ +package cats +package laws +package discipline + +import org.scalacheck.{Arbitrary, Cogen, Prop} +import Prop._ + +import cats.data.Ior +import org.typelevel.discipline.Laws + +trait AlignTests[F[_]] extends Laws { + def laws: AlignLaws[F] + + def align[A: Arbitrary, B: Arbitrary, C: Arbitrary, D: Arbitrary]( + implicit ArbFA: Arbitrary[F[A]], + ArbFB: Arbitrary[F[B]], + ArbFC: Arbitrary[F[C]], + ArbFAtoB: Arbitrary[A => C], + ArbFBtoC: Arbitrary[B => D], + ArbIorABtoC: Arbitrary[A Ior B => C], + CogenA: Cogen[A], + CogenB: Cogen[B], + CogenC: Cogen[C], + EqFA: Eq[F[A]], + EqFB: Eq[F[B]], + EqFC: Eq[F[C]], + EqFIorAA: Eq[F[A Ior A]], + EqFIorAB: Eq[F[A Ior B]], + EqFIorCD: Eq[F[C Ior D]], + EqFAssoc: Eq[F[Ior[Ior[A, B], C]]] + ): RuleSet = new DefaultRuleSet( + name = "align", + parent = None, + "align associativity" -> forAll(laws.alignAssociativity[A, B, C] _), + "align self both" -> forAll(laws.alignSelfBoth[A] _), + "align homomorphism" -> forAll { (fa: F[A], fb: F[B], f: A => C, g: B => D) => + laws.alignHomomorphism[A, B, C, D](fa, fb, f, g) + }, + "alignWith consistent" -> forAll { (fa: F[A], fb: F[B], f: A Ior B => C) => + laws.alignWithConsistent[A, B, C](fa, fb, f) + }) +} + +object AlignTests { + def apply[F[_]: Align]: AlignTests[F] = + new AlignTests[F] { def laws: AlignLaws[F] = AlignLaws[F] } +} diff --git a/tests/src/test/scala-2.13+/cats/tests/LazyListSuite.scala b/tests/src/test/scala-2.13+/cats/tests/LazyListSuite.scala index 5392187a2c..bfa3267f66 100644 --- a/tests/src/test/scala-2.13+/cats/tests/LazyListSuite.scala +++ b/tests/src/test/scala-2.13+/cats/tests/LazyListSuite.scala @@ -2,6 +2,7 @@ package cats package tests import cats.laws.discipline.{ + AlignTests, AlternativeTests, CoflatMapTests, CommutativeApplyTests, @@ -33,6 +34,9 @@ class LazyListSuite extends CatsSuite { checkAll("LazyList[Int]", TraverseFilterTests[LazyList].traverseFilter[Int, Int, Int]) checkAll("TraverseFilter[LazyList]", SerializableTests.serializable(TraverseFilter[LazyList])) + checkAll("LazyList[Int]", AlignTests[LazyList].align[Int, Int, Int, Int]) + checkAll("Align[LazyList]", SerializableTests.serializable(Align[LazyList])) + // Can't test applicative laws as they don't terminate checkAll("ZipLazyList[Int]", CommutativeApplyTests[ZipLazyList].apply[Int, Int, Int]) diff --git a/tests/src/test/scala-2.13+/cats/tests/NonEmptyLazyListSuite.scala b/tests/src/test/scala-2.13+/cats/tests/NonEmptyLazyListSuite.scala index 26edbf10cd..36a81c9bef 100644 --- a/tests/src/test/scala-2.13+/cats/tests/NonEmptyLazyListSuite.scala +++ b/tests/src/test/scala-2.13+/cats/tests/NonEmptyLazyListSuite.scala @@ -3,7 +3,7 @@ package tests import cats.data.NonEmptyLazyList import cats.kernel.laws.discipline.{EqTests, HashTests, OrderTests, PartialOrderTests, SemigroupTests} -import cats.laws.discipline.{BimonadTests, NonEmptyTraverseTests, SemigroupKTests, SerializableTests} +import cats.laws.discipline.{AlignTests,BimonadTests, NonEmptyTraverseTests, SemigroupKTests, SerializableTests} import cats.laws.discipline.arbitrary._ class NonEmptyLazyListSuite extends CatsSuite { @@ -27,6 +27,9 @@ class NonEmptyLazyListSuite extends CatsSuite { checkAll("NonEmptyLazyList[Int]", OrderTests[NonEmptyLazyList[Int]].order) checkAll("Order[NonEmptyLazyList[Int]", SerializableTests.serializable(Order[NonEmptyLazyList[Int]])) + checkAll("NonEmptyLazyList[Int]", AlignTests[NonEmptyLazyList].align[Int, Int, Int, Int]) + checkAll("Align[NonEmptyLazyList]", SerializableTests.serializable(Align[NonEmptyLazyList])) + test("show") { Show[NonEmptyLazyList[Int]].show(NonEmptyLazyList(1, 2, 3)) should ===("NonEmptyLazyList(1, ?)") } diff --git a/tests/src/test/scala/cats/tests/ChainSuite.scala b/tests/src/test/scala/cats/tests/ChainSuite.scala index ea7de72845..8b77eb6832 100644 --- a/tests/src/test/scala/cats/tests/ChainSuite.scala +++ b/tests/src/test/scala/cats/tests/ChainSuite.scala @@ -5,6 +5,7 @@ import cats.data.Chain import cats.data.Chain.==: import cats.data.Chain.`:==` import cats.laws.discipline.{ + AlignTests, AlternativeTests, CoflatMapTests, MonadTests, @@ -34,6 +35,9 @@ class ChainSuite extends CatsSuite { checkAll("Chain[Int]", OrderTests[Chain[Int]].order) checkAll("Order[Chain]", SerializableTests.serializable(Order[Chain[Int]])) + checkAll("Chain[Int]", AlignTests[Chain].align[Int, Int, Int, Int]) + checkAll("Align[Chain]", SerializableTests.serializable(Align[Chain])) + checkAll("Chain[Int]", TraverseFilterTests[Chain].traverseFilter[Int, Int, Int]) checkAll("TraverseFilter[Chain]", SerializableTests.serializable(TraverseFilter[Chain])) diff --git a/tests/src/test/scala/cats/tests/ListSuite.scala b/tests/src/test/scala/cats/tests/ListSuite.scala index c5301365f1..64d9ee494a 100644 --- a/tests/src/test/scala/cats/tests/ListSuite.scala +++ b/tests/src/test/scala/cats/tests/ListSuite.scala @@ -3,6 +3,7 @@ package tests import cats.data.{NonEmptyList, ZipList} import cats.laws.discipline.{ + AlignTests, AlternativeTests, CoflatMapTests, CommutativeApplyTests, @@ -34,6 +35,9 @@ class ListSuite extends CatsSuite { checkAll("List[Int]", TraverseFilterTests[List].traverseFilter[Int, Int, Int]) checkAll("TraverseFilter[List]", SerializableTests.serializable(TraverseFilter[List])) + checkAll("List[Int]", AlignTests[List].align[Int, Int, Int, Int]) + checkAll("Align[List]", SerializableTests.serializable(Align[List])) + checkAll("ZipList[Int]", CommutativeApplyTests[ZipList].commutativeApply[Int, Int, Int]) test("nel => list => nel returns original nel")( diff --git a/tests/src/test/scala/cats/tests/MapSuite.scala b/tests/src/test/scala/cats/tests/MapSuite.scala index bddf9b5805..5dcf86acfc 100644 --- a/tests/src/test/scala/cats/tests/MapSuite.scala +++ b/tests/src/test/scala/cats/tests/MapSuite.scala @@ -2,6 +2,7 @@ package cats package tests import cats.laws.discipline.{ + AlignTests, ComposeTests, FlatMapTests, FunctorFilterTests, @@ -35,6 +36,9 @@ class MapSuite extends CatsSuite { checkAll("Map[Int, Int]", MonoidKTests[Map[Int, *]].monoidK[Int]) checkAll("MonoidK[Map[Int, *]]", SerializableTests.serializable(MonoidK[Map[Int, *]])) + checkAll("Map[Int, Int]", AlignTests[Map[Int, ?]].align[Int, Int, Int, Int]) + checkAll("Align[Map]", SerializableTests.serializable(Align[Map[Int, ?]])) + test("show isn't empty and is formatted as expected") { forAll { (map: Map[Int, String]) => map.show.nonEmpty should ===(true) diff --git a/tests/src/test/scala/cats/tests/NonEmptyChainSuite.scala b/tests/src/test/scala/cats/tests/NonEmptyChainSuite.scala index 2d76aa261b..2107825c12 100644 --- a/tests/src/test/scala/cats/tests/NonEmptyChainSuite.scala +++ b/tests/src/test/scala/cats/tests/NonEmptyChainSuite.scala @@ -3,7 +3,7 @@ package tests import cats.data.{Chain, NonEmptyChain} import cats.kernel.laws.discipline.{EqTests, OrderTests, PartialOrderTests, SemigroupTests} -import cats.laws.discipline.{BimonadTests, NonEmptyTraverseTests, SemigroupKTests, SerializableTests} +import cats.laws.discipline.{AlignTests, BimonadTests, NonEmptyTraverseTests, SemigroupKTests, SerializableTests} import cats.laws.discipline.arbitrary._ class NonEmptyChainSuite extends CatsSuite { @@ -23,6 +23,9 @@ class NonEmptyChainSuite extends CatsSuite { checkAll("NonEmptyChain[Int]", OrderTests[NonEmptyChain[Int]].order) checkAll("Order[NonEmptyChain[Int]", SerializableTests.serializable(Order[NonEmptyChain[Int]])) + checkAll("NonEmptyChain[Int]", AlignTests[NonEmptyChain].align[Int, Int, Int, Int]) + checkAll("Align[NonEmptyChain]", SerializableTests.serializable(Align[NonEmptyChain])) + { implicit val partialOrder = ListWrapper.partialOrder[Int] checkAll("NonEmptyChain[ListWrapper[Int]]", PartialOrderTests[NonEmptyChain[ListWrapper[Int]]].partialOrder) diff --git a/tests/src/test/scala/cats/tests/NonEmptyListSuite.scala b/tests/src/test/scala/cats/tests/NonEmptyListSuite.scala index b26c6ba674..acfbd5d937 100644 --- a/tests/src/test/scala/cats/tests/NonEmptyListSuite.scala +++ b/tests/src/test/scala/cats/tests/NonEmptyListSuite.scala @@ -7,6 +7,7 @@ import cats.data.{NonEmptyList, NonEmptyMap, NonEmptySet, NonEmptyVector} import cats.data.NonEmptyList.ZipNonEmptyList import cats.laws.discipline.arbitrary._ import cats.laws.discipline.{ + AlignTests, BimonadTests, CommutativeApplyTests, NonEmptyTraverseTests, @@ -43,6 +44,9 @@ class NonEmptyListSuite extends CatsSuite { checkAll("NonEmptyList[ListWrapper[Int]]", EqTests[NonEmptyList[ListWrapper[Int]]].eqv) checkAll("Eq[NonEmptyList[ListWrapper[Int]]]", SerializableTests.serializable(Eq[NonEmptyList[ListWrapper[Int]]])) + checkAll("NonEmptyList[Int]", AlignTests[NonEmptyList].align[Int, Int, Int, Int]) + checkAll("Align[NonEmptyList]", SerializableTests.serializable(Align[NonEmptyList])) + checkAll("ZipNonEmptyList[Int]", CommutativeApplyTests[ZipNonEmptyList].commutativeApply[Int, Int, Int]) { diff --git a/tests/src/test/scala/cats/tests/NonEmptyMapSuite.scala b/tests/src/test/scala/cats/tests/NonEmptyMapSuite.scala index 2b61a76f02..13be37b594 100644 --- a/tests/src/test/scala/cats/tests/NonEmptyMapSuite.scala +++ b/tests/src/test/scala/cats/tests/NonEmptyMapSuite.scala @@ -20,7 +20,7 @@ package tests import cats.laws.discipline._ import cats.laws.discipline.arbitrary._ import cats.data._ -import cats.kernel.laws.discipline._ +import cats.kernel.laws.discipline.{SerializableTests => _, _} import scala.collection.immutable.SortedMap @@ -35,6 +35,9 @@ class NonEmptyMapSuite extends CatsSuite { checkAll("NonEmptyMap[String, Int]", EqTests[NonEmptyMap[String, Int]].eqv) checkAll("NonEmptyMap[String, Int]", HashTests[NonEmptyMap[String, Int]].hash) + checkAll("NonEmptyMap[String, Int]", AlignTests[NonEmptyMap[String, *]].align[Int, Int, Int, Int]) + checkAll("Align[NonEmptyMap]", SerializableTests.serializable(Align[NonEmptyMap[String, *]])) + test("Show is not empty and is formatted as expected") { forAll { (nem: NonEmptyMap[String, Int]) => nem.show.nonEmpty should ===(true) diff --git a/tests/src/test/scala/cats/tests/NonEmptyVectorSuite.scala b/tests/src/test/scala/cats/tests/NonEmptyVectorSuite.scala index 226c809dd0..056aaa69cc 100644 --- a/tests/src/test/scala/cats/tests/NonEmptyVectorSuite.scala +++ b/tests/src/test/scala/cats/tests/NonEmptyVectorSuite.scala @@ -7,6 +7,7 @@ import cats.kernel.laws.discipline.{EqTests, SemigroupTests} import cats.data.NonEmptyVector import cats.laws.discipline.{ + AlignTests, BimonadTests, CommutativeApplyTests, FoldableTests, @@ -44,6 +45,9 @@ class NonEmptyVectorSuite extends CatsSuite { checkAll("NonEmptyVector[Int]", FoldableTests[NonEmptyVector].foldable[Int, Int]) checkAll("Foldable[NonEmptyVector]", SerializableTests.serializable(Foldable[NonEmptyVector])) + checkAll("NonEmptyVector[Int]", AlignTests[NonEmptyVector].align[Int, Int, Int, Int]) + checkAll("Align[NonEmptyVector]", SerializableTests.serializable(Align[NonEmptyVector])) + checkAll("ZipNonEmptyVector[Int]", CommutativeApplyTests[ZipNonEmptyVector].commutativeApply[Int, Int, Int]) checkAll("CommutativeApply[ZipNonEmptyVector]", SerializableTests.serializable(CommutativeApply[ZipNonEmptyVector])) diff --git a/tests/src/test/scala/cats/tests/SortedMapSuite.scala b/tests/src/test/scala/cats/tests/SortedMapSuite.scala index 3fa21a142b..be93029f9f 100644 --- a/tests/src/test/scala/cats/tests/SortedMapSuite.scala +++ b/tests/src/test/scala/cats/tests/SortedMapSuite.scala @@ -4,6 +4,7 @@ package tests import cats.kernel.CommutativeMonoid import cats.kernel.laws.discipline.{CommutativeMonoidTests, HashTests, MonoidTests} import cats.laws.discipline.{ + AlignTests, FlatMapTests, MonoidKTests, SemigroupalTests, @@ -31,6 +32,9 @@ class SortedMapSuite extends CatsSuite { checkAll("SortedMap[Int, Int]", TraverseFilterTests[SortedMap[Int, *]].traverseFilter[Int, Int, Int]) checkAll("TraverseFilter[SortedMap[Int, *]]", SerializableTests.serializable(TraverseFilter[SortedMap[Int, *]])) + checkAll("SortedMap[Int, Int]", AlignTests[SortedMap[Int, *]].align[Int, Int, Int, Int]) + checkAll("Align[SortedMap[Int, *]]", SerializableTests.serializable(Align[SortedMap[Int, *]])) + test("show isn't empty and is formatted as expected") { forAll { (map: SortedMap[Int, String]) => map.show.nonEmpty should ===(true) diff --git a/tests/src/test/scala/cats/tests/SyntaxSuite.scala b/tests/src/test/scala/cats/tests/SyntaxSuite.scala index 5374ea4af5..f2d20fa70a 100644 --- a/tests/src/test/scala/cats/tests/SyntaxSuite.scala +++ b/tests/src/test/scala/cats/tests/SyntaxSuite.scala @@ -428,6 +428,23 @@ object SyntaxSuite val grouped: SortedMap[B, NonEmptySet[A]] = set.groupByNes(f) } + def testAlign[F[_]: Align, A, B, C]: Unit = { + import cats.data.Ior + val fa = mock[F[A]] + val fb = mock[F[B]] + val f = mock[A Ior B => C] + val f2 = mock[(Option[A], Option[B]) => C] + + val fab = fa.align(fb) + val fc = fa.alignWith(fb)(f) + + val padZipped = fa.padZip(fb) + val padZippedWith = fa.padZipWith(fb)(f2) + + implicit val sa = mock[Semigroup[A]] + val fa2 = fa.alignCombine(fa) + } + def testNonEmptyList[A, B: Order]: Unit = { val f = mock[A => B] val list = mock[List[A]] diff --git a/tests/src/test/scala/cats/tests/VectorSuite.scala b/tests/src/test/scala/cats/tests/VectorSuite.scala index 0f89646539..f82bce3df7 100644 --- a/tests/src/test/scala/cats/tests/VectorSuite.scala +++ b/tests/src/test/scala/cats/tests/VectorSuite.scala @@ -3,6 +3,7 @@ package tests import cats.data.{NonEmptyVector, ZipVector} import cats.laws.discipline.{ + AlignTests, AlternativeTests, CoflatMapTests, CommutativeApplyTests, @@ -33,6 +34,9 @@ class VectorSuite extends CatsSuite { checkAll("Vector[Int]", TraverseFilterTests[Vector].traverseFilter[Int, Int, Int]) checkAll("TraverseFilter[Vector]", SerializableTests.serializable(TraverseFilter[Vector])) + checkAll("Vector[Int]", AlignTests[Vector].align[Int, Int, Int, Int]) + checkAll("Align[Vector]", SerializableTests.serializable(Align[Vector])) + checkAll("ZipVector[Int]", CommutativeApplyTests[ZipVector].commutativeApply[Int, Int, Int]) test("show") {