From 2523014f4bc40daac47d9a5866ce7baffd7389af Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Fri, 20 Sep 2019 14:09:25 -0400 Subject: [PATCH 1/8] Add Align typeclass --- .gitignore | 1 + build.sbt | 3 + .../main/scala-2.12/cats/compat/Vector.scala | 6 ++ .../main/scala-2.13+/cats/compat/Vector.scala | 6 ++ .../cats/data/NonEmptyLazyList.scala | 50 ++++++------- .../scala-2.13+/cats/instances/lazyList.scala | 27 ++++++- core/src/main/scala/cats/Align.scala | 47 +++++++++++++ .../cats/data/AbstractNonEmptyInstances.scala | 14 +++- core/src/main/scala/cats/data/Chain.scala | 25 ++++++- .../main/scala/cats/data/NonEmptyChain.scala | 70 ++++++++++--------- .../main/scala/cats/data/NonEmptyList.scala | 23 +++++- .../scala/cats/data/NonEmptyMapImpl.scala | 9 ++- .../main/scala/cats/data/NonEmptyVector.scala | 17 ++++- .../main/scala/cats/instances/either.scala | 25 ++++++- core/src/main/scala/cats/instances/list.scala | 24 ++++++- core/src/main/scala/cats/instances/map.scala | 26 ++++++- .../main/scala/cats/instances/option.scala | 19 ++++- .../main/scala/cats/instances/sortedMap.scala | 29 +++++++- .../main/scala/cats/instances/vector.scala | 16 ++++- core/src/main/scala/cats/syntax/align.scala | 4 ++ core/src/main/scala/cats/syntax/all.scala | 1 + core/src/main/scala/cats/syntax/package.scala | 1 + laws/src/main/scala/cats/laws/AlignLaws.scala | 48 +++++++++++++ .../cats/laws/discipline/AlignTests.scala | 47 +++++++++++++ .../cats/tests/LazyListSuite.scala | 4 ++ .../cats/tests/NonEmptyLazyListSuite.scala | 5 +- .../test/scala/cats/tests/ChainSuite.scala | 4 ++ .../src/test/scala/cats/tests/ListSuite.scala | 4 ++ .../src/test/scala/cats/tests/MapSuite.scala | 4 ++ .../scala/cats/tests/NonEmptyChainSuite.scala | 5 +- .../scala/cats/tests/NonEmptyListSuite.scala | 4 ++ .../scala/cats/tests/NonEmptyMapSuite.scala | 5 +- .../cats/tests/NonEmptyVectorSuite.scala | 4 ++ .../scala/cats/tests/SortedMapSuite.scala | 4 ++ .../test/scala/cats/tests/SyntaxSuite.scala | 17 +++++ .../test/scala/cats/tests/VectorSuite.scala | 4 ++ 36 files changed, 515 insertions(+), 87 deletions(-) create mode 100644 core/src/main/scala-2.12/cats/compat/Vector.scala create mode 100644 core/src/main/scala-2.13+/cats/compat/Vector.scala create mode 100644 core/src/main/scala/cats/Align.scala create mode 100644 core/src/main/scala/cats/syntax/align.scala create mode 100644 laws/src/main/scala/cats/laws/AlignLaws.scala create mode 100644 laws/src/main/scala/cats/laws/discipline/AlignTests.scala 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") { From 909a33a797b0e2c06ac1da5622c3225fa443e5ef Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Fri, 20 Sep 2019 15:01:14 -0400 Subject: [PATCH 2/8] Scalafmt --- build.sbt | 2 +- .../main/scala-2.12/cats/compat/Vector.scala | 2 +- .../main/scala/cats/data/NonEmptyChain.scala | 64 +++++++++---------- laws/src/main/scala/cats/laws/AlignLaws.scala | 24 +++---- .../cats/laws/discipline/AlignTests.scala | 22 +++---- 5 files changed, 58 insertions(+), 56 deletions(-) diff --git a/build.sbt b/build.sbt index 19a1a1aea7..604b60fdc5 100644 --- a/build.sbt +++ b/build.sbt @@ -398,7 +398,7 @@ def mimaSettings(moduleName: String) = exclude[MissingClassProblem]( "cats.kernel.compat.scalaVersionMoreSpecific$suppressUnusedImportWarningForScalaVersionMoreSpecific" ) - ) ++ //abstract package private classes + ) ++ //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 index 5970fc959c..31917aa3bf 100644 --- a/core/src/main/scala-2.12/cats/compat/Vector.scala +++ b/core/src/main/scala-2.12/cats/compat/Vector.scala @@ -3,4 +3,4 @@ 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/cats/data/NonEmptyChain.scala b/core/src/main/scala/cats/data/NonEmptyChain.scala index a5a736b6f3..69a232e719 100644 --- a/core/src/main/scala/cats/data/NonEmptyChain.scala +++ b/core/src/main/scala/cats/data/NonEmptyChain.scala @@ -422,38 +422,38 @@ sealed abstract private[data] class NonEmptyChainInstances extends 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) - } + 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/laws/src/main/scala/cats/laws/AlignLaws.scala b/laws/src/main/scala/cats/laws/AlignLaws.scala index b21fcd36b8..1e81955206 100644 --- a/laws/src/main/scala/cats/laws/AlignLaws.scala +++ b/laws/src/main/scala/cats/laws/AlignLaws.scala @@ -5,7 +5,7 @@ import cats.syntax.align._ import cats.syntax.functor._ import cats.data.Ior -import cats.data.Ior.{Left, Right, Both} +import cats.data.Ior.{Both, Left, Right} /** * Laws that must be obeyed by any `Align`. @@ -29,16 +29,18 @@ trait AlignLaws[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) - } + 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) + } } } diff --git a/laws/src/main/scala/cats/laws/discipline/AlignTests.scala b/laws/src/main/scala/cats/laws/discipline/AlignTests.scala index 932a3eb1cd..80f0dd13ed 100644 --- a/laws/src/main/scala/cats/laws/discipline/AlignTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/AlignTests.scala @@ -28,17 +28,17 @@ trait AlignTests[F[_]] extends Laws { 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) - }) + ): 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 { From c5084bf7516b0247f83c7344ed6a7ede47b43cd9 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Fri, 20 Sep 2019 23:55:43 -0400 Subject: [PATCH 3/8] Add Const and Validated instances and derivation from SemigroupK and Apply --- core/src/main/scala/cats/Align.scala | 6 ++++++ core/src/main/scala/cats/Apply.scala | 6 ++++++ core/src/main/scala/cats/SemigroupK.scala | 9 ++++++++ core/src/main/scala/cats/data/Const.scala | 6 ++++++ core/src/main/scala/cats/data/Validated.scala | 21 +++++++++++++++++++ laws/src/main/scala/cats/laws/AlignLaws.scala | 5 +---- .../cats/laws/discipline/AlignTests.scala | 1 - .../test/scala/cats/tests/AlignSuite.scala | 14 +++++++++++++ .../scala/cats/tests/ApplicativeSuite.scala | 10 +++++++++ .../test/scala/cats/tests/ConstSuite.scala | 3 +++ .../scala/cats/tests/SemigroupKSuite.scala | 20 ++++++++++++++++++ .../scala/cats/tests/ValidatedSuite.scala | 3 +++ 12 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 tests/src/test/scala/cats/tests/AlignSuite.scala create mode 100644 tests/src/test/scala/cats/tests/SemigroupKSuite.scala diff --git a/core/src/main/scala/cats/Align.scala b/core/src/main/scala/cats/Align.scala index a6b890c681..884bda6ece 100644 --- a/core/src/main/scala/cats/Align.scala +++ b/core/src/main/scala/cats/Align.scala @@ -45,3 +45,9 @@ import cats.data.Ior 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)) } + +object Align { + def semigroup[F[_]: Align, A: Semigroup]: Semigroup[F[A]] = new Semigroup[F[A]] { + def combine(x: F[A], y: F[A]): F[A] = Align[F].alignCombine(x, y) + } +} diff --git a/core/src/main/scala/cats/Apply.scala b/core/src/main/scala/cats/Apply.scala index f618cd8df9..3a43f93a43 100644 --- a/core/src/main/scala/cats/Apply.scala +++ b/core/src/main/scala/cats/Apply.scala @@ -2,6 +2,7 @@ package cats import simulacrum.typeclass import simulacrum.noop +import cats.data.Ior /** * Weaker version of Applicative[F]; has apply but not pure. @@ -225,6 +226,11 @@ object Apply { */ def semigroup[F[_], A](implicit f: Apply[F], sg: Semigroup[A]): Semigroup[F[A]] = new ApplySemigroup[F, A](f, sg) + + def align[F[_]: Apply]: Align[F] = new Align[F] { + def align[A, B](fa: F[A], fb: F[B]): F[Ior[A, B]] = Apply[F].map2(fa, fb)(Ior.both) + def functor: Functor[F] = Apply[F] + } } private[cats] class ApplySemigroup[F[_], A](f: Apply[F], sg: Semigroup[A]) extends Semigroup[F[A]] { diff --git a/core/src/main/scala/cats/SemigroupK.scala b/core/src/main/scala/cats/SemigroupK.scala index f3ca1b5965..4aba7ba023 100644 --- a/core/src/main/scala/cats/SemigroupK.scala +++ b/core/src/main/scala/cats/SemigroupK.scala @@ -1,6 +1,7 @@ package cats import simulacrum.typeclass +import cats.data.Ior /** * SemigroupK is a universal semigroup which operates on kinds. @@ -68,3 +69,11 @@ import simulacrum.typeclass val F = self } } + +object SemigroupK { + def align[F[_]: SemigroupK: Functor]: Align[F] = new Align[F] { + def align[A, B](fa: F[A], fb: F[B]): F[Ior[A, B]] = + SemigroupK[F].combineK(Functor[F].map(fa)(Ior.left), Functor[F].map(fb)(Ior.right)) + def functor: Functor[F] = Functor[F] + } +} diff --git a/core/src/main/scala/cats/data/Const.scala b/core/src/main/scala/cats/data/Const.scala index 90a637b116..c8e1e6aba4 100644 --- a/core/src/main/scala/cats/data/Const.scala +++ b/core/src/main/scala/cats/data/Const.scala @@ -74,6 +74,12 @@ sealed abstract private[data] class ConstInstances extends ConstInstances0 { x.compare(y) } + implicit def catsDataAlignForConst[A: Semigroup]: Align[Const[A, *]] = new Align[Const[A, *]] { + def align[B, C](fa: Const[A, B], fb: Const[A, C]): Const[A, Ior[B, C]] = + Const(Semigroup[A].combine(fa.getConst, fb.getConst)) + def functor: Functor[Const[A, *]] = catsDataFunctorForConst + } + implicit def catsDataShowForConst[A: Show, B]: Show[Const[A, B]] = new Show[Const[A, B]] { def show(f: Const[A, B]): String = f.show } diff --git a/core/src/main/scala/cats/data/Validated.scala b/core/src/main/scala/cats/data/Validated.scala index 7805497b7a..ee0ffa3214 100644 --- a/core/src/main/scala/cats/data/Validated.scala +++ b/core/src/main/scala/cats/data/Validated.scala @@ -371,6 +371,27 @@ sealed abstract private[data] class ValidatedInstances extends ValidatedInstance } } + implicit def catsDataAlignForValidated[E: Semigroup]: Align[Validated[E, *]] = + new Align[Validated[E, *]] { + def functor: Functor[Validated[E, *]] = catsDataTraverseFunctorForValidated + def align[A, B](fa: Validated[E, A], fb: Validated[E, B]): Validated[E, Ior[A, B]] = + alignWith(fa, fb)(identity) + + override def alignWith[A, B, C](fa: Validated[E, A], fb: Validated[E, B])(f: Ior[A, B] => C): Validated[E, C] = + fa match { + case Invalid(e) => + fb match { + case Invalid(e2) => Invalid(Semigroup[E].combine(e, e2)) + case Valid(b) => Valid(f(Ior.right(b))) + } + case Valid(a) => + fb match { + case Invalid(e) => Valid(f(Ior.left(a))) + case Valid(b) => Valid(f(Ior.both(a, b))) + } + } + } + implicit def catsDataMonoidForValidated[A, B](implicit A: Semigroup[A], B: Monoid[B]): Monoid[Validated[A, B]] = new Monoid[Validated[A, B]] { def empty: Validated[A, B] = Valid(B.empty) diff --git a/laws/src/main/scala/cats/laws/AlignLaws.scala b/laws/src/main/scala/cats/laws/AlignLaws.scala index 1e81955206..590a11fb79 100644 --- a/laws/src/main/scala/cats/laws/AlignLaws.scala +++ b/laws/src/main/scala/cats/laws/AlignLaws.scala @@ -18,10 +18,7 @@ trait AlignLaws[F[_]] { 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]] = + def alignHomomorphism[A, B, C, D](fa: F[A], fb: F[B], f: A => C, g: B => D): IsEq[F[Ior[C, 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]] = diff --git a/laws/src/main/scala/cats/laws/discipline/AlignTests.scala b/laws/src/main/scala/cats/laws/discipline/AlignTests.scala index 80f0dd13ed..8d35bb3acf 100644 --- a/laws/src/main/scala/cats/laws/discipline/AlignTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/AlignTests.scala @@ -32,7 +32,6 @@ trait AlignTests[F[_]] extends Laws { 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) }, diff --git a/tests/src/test/scala/cats/tests/AlignSuite.scala b/tests/src/test/scala/cats/tests/AlignSuite.scala new file mode 100644 index 0000000000..167bc0e760 --- /dev/null +++ b/tests/src/test/scala/cats/tests/AlignSuite.scala @@ -0,0 +1,14 @@ +package cats.tests + +import cats.Align +import cats.kernel.laws.discipline.SemigroupTests + +class AlignSuite extends CatsSuite { + { + val optionSemigroup = Align.semigroup[Option, Int] + checkAll("Align[Option].semigroup", SemigroupTests[Option[Int]](optionSemigroup).semigroup) + + val listSemigroup = Align.semigroup[List, String] + checkAll("Align[List].semigroup", SemigroupTests[List[String]](listSemigroup).semigroup) + } +} diff --git a/tests/src/test/scala/cats/tests/ApplicativeSuite.scala b/tests/src/test/scala/cats/tests/ApplicativeSuite.scala index e500697cfb..d85e050038 100644 --- a/tests/src/test/scala/cats/tests/ApplicativeSuite.scala +++ b/tests/src/test/scala/cats/tests/ApplicativeSuite.scala @@ -6,6 +6,7 @@ import cats.kernel.laws.discipline.{MonoidTests, SemigroupTests} import cats.data.{Const, Validated} import cats.laws.discipline.arbitrary._ import cats.laws.discipline.CoflatMapTests +import cats.laws.discipline.AlignTests class ApplicativeSuite extends CatsSuite { @@ -59,6 +60,15 @@ class ApplicativeSuite extends CatsSuite { implicit val constCoflatMap = Applicative.coflatMap[Const[String, *]] checkAll("Applicative[Const].coflatMap", CoflatMapTests[Const[String, *]].coflatMap[String, String, String]) + + implicit val listwrapperAlign = Apply.align[ListWrapper] + checkAll("Apply[ListWrapper].align", AlignTests[ListWrapper].align[Int, Int, Int, Int]) + + implicit val validatedAlign = Apply.align[Validated[String, *]] + checkAll("Apply[Validated].align", AlignTests[Validated[String, *]].align[Int, Int, Int, Int]) + + implicit val constAlign = Apply.align[Const[String, *]] + checkAll("Apply[Const].align", AlignTests[Const[String, *]].align[Int, Int, Int, Int]) } } diff --git a/tests/src/test/scala/cats/tests/ConstSuite.scala b/tests/src/test/scala/cats/tests/ConstSuite.scala index d7e5477ce9..4427328259 100644 --- a/tests/src/test/scala/cats/tests/ConstSuite.scala +++ b/tests/src/test/scala/cats/tests/ConstSuite.scala @@ -32,6 +32,9 @@ class ConstSuite extends CatsSuite { checkAll("Const[String, Int]", TraverseFilterTests[Const[String, *]].traverseFilter[Int, Int, Int]) checkAll("TraverseFilter[Const[String, *]]", SerializableTests.serializable(TraverseFilter[Const[String, *]])) + checkAll("Const[String, Int]", AlignTests[Const[String, *]].align[Int, Int, Int, Int]) + checkAll("Align[Const[String, *]]", SerializableTests.serializable(Align[Const[String, *]])) + // Get Apply[Const[C : Semigroup, *]], not Applicative[Const[C : Monoid, *]] { implicit def nonEmptyListSemigroup[A]: Semigroup[NonEmptyList[A]] = SemigroupK[NonEmptyList].algebra diff --git a/tests/src/test/scala/cats/tests/SemigroupKSuite.scala b/tests/src/test/scala/cats/tests/SemigroupKSuite.scala new file mode 100644 index 0000000000..fa30726f57 --- /dev/null +++ b/tests/src/test/scala/cats/tests/SemigroupKSuite.scala @@ -0,0 +1,20 @@ +package cats.tests + +import cats.SemigroupK +import cats.data.{Chain, Validated} +import cats.laws.discipline.AlignTests +import cats.laws.discipline.arbitrary._ + +class SemigroupKSuite extends CatsSuite { + { + implicit val listwrapperSemigroupK = ListWrapper.alternative + implicit val listwrapperAlign = SemigroupK.align[ListWrapper] + checkAll("SemigroupK[ListWrapper].align", AlignTests[ListWrapper].align[Int, Int, Int, Int]) + + implicit val validatedAlign = SemigroupK.align[Validated[String, *]] + checkAll("SemigroupK[Validated].align", AlignTests[Validated[String, *]].align[Int, Int, Int, Int]) + + implicit val chainAlign = SemigroupK.align[Chain] + checkAll("SemigroupK[Chain].align", AlignTests[Chain].align[Int, Int, Int, Int]) + } +} diff --git a/tests/src/test/scala/cats/tests/ValidatedSuite.scala b/tests/src/test/scala/cats/tests/ValidatedSuite.scala index a7bc31cb29..1a27de9dda 100644 --- a/tests/src/test/scala/cats/tests/ValidatedSuite.scala +++ b/tests/src/test/scala/cats/tests/ValidatedSuite.scala @@ -41,6 +41,9 @@ class ValidatedSuite extends CatsSuite { checkAll("CommutativeApplicative[Validated[Int, *]]", SerializableTests.serializable(CommutativeApplicative[Validated[Int, *]])) + checkAll("Validated[Int, Int]", AlignTests[Validated[Int, *]].align[Int, Int, Int, Int]) + checkAll("Align[Validated[Int, *]]", SerializableTests.serializable(Align[Validated[Int, *]])) + { implicit val L = ListWrapper.semigroup[String] checkAll("Validated[ListWrapper[String], *]", SemigroupKTests[Validated[ListWrapper[String], *]].semigroupK[Int]) From 006a23cb9b3339196d23cf6529564e14c1e7b542 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Thu, 10 Oct 2019 10:29:22 -0400 Subject: [PATCH 4/8] Add MiMaException tests --- binCompatTest/src/main/scala/catsBC/MimaExceptions.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/binCompatTest/src/main/scala/catsBC/MimaExceptions.scala b/binCompatTest/src/main/scala/catsBC/MimaExceptions.scala index 91bcae72be..ad17ec0eca 100644 --- a/binCompatTest/src/main/scala/catsBC/MimaExceptions.scala +++ b/binCompatTest/src/main/scala/catsBC/MimaExceptions.scala @@ -29,6 +29,11 @@ object MimaExceptions { Either.catchOnly[NumberFormatException] { "foo".toInt }, (1.validNel[String], 2.validNel[String], 3.validNel[String]) mapN (_ + _ + _), (1.asRight[String], 2.asRight[String], 3.asRight[String]) parMapN (_ + _ + _), - InjectK.catsReflexiveInjectKInstance[Option] + InjectK.catsReflexiveInjectKInstance[Option], + ( + cats.Bimonad[cats.data.NonEmptyChain], + cats.NonEmptyTraverse[cats.data.NonEmptyChain], + cats.SemigroupK[cats.data.NonEmptyChain] + ) ) } From 48eaa1410916a9a50e22d2634fd0200788449dd1 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Thu, 10 Oct 2019 10:31:31 -0400 Subject: [PATCH 5/8] scalafmt --- core/src/main/scala/cats/data/NonEmptyList.scala | 7 +++++-- core/src/main/scala/cats/data/NonEmptyVector.scala | 9 +++++---- core/src/main/scala/cats/instances/option.scala | 8 ++++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/core/src/main/scala/cats/data/NonEmptyList.scala b/core/src/main/scala/cats/data/NonEmptyList.scala index fe1f58758b..9bea4dc4d4 100644 --- a/core/src/main/scala/cats/data/NonEmptyList.scala +++ b/core/src/main/scala/cats/data/NonEmptyList.scala @@ -511,8 +511,11 @@ sealed abstract private[data] class NonEmptyListInstances extends NonEmptyListIn implicit val catsDataInstancesForNonEmptyList : 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 Align[NonEmptyList] { + new NonEmptyReducible[NonEmptyList, List] + with SemigroupK[NonEmptyList] + with Bimonad[NonEmptyList] + with NonEmptyTraverse[NonEmptyList] + with Align[NonEmptyList] { def combineK[A](a: NonEmptyList[A], b: NonEmptyList[A]): NonEmptyList[A] = a.concatNel(b) diff --git a/core/src/main/scala/cats/data/NonEmptyVector.scala b/core/src/main/scala/cats/data/NonEmptyVector.scala index 8904bbac75..07f2324f75 100644 --- a/core/src/main/scala/cats/data/NonEmptyVector.scala +++ b/core/src/main/scala/cats/data/NonEmptyVector.scala @@ -238,14 +238,15 @@ 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] with Align[NonEmptyVector] = - new NonEmptyReducible[NonEmptyVector, Vector] with 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 Align[NonEmptyVector] { def combineK[A](a: NonEmptyVector[A], b: NonEmptyVector[A]): NonEmptyVector[A] = a.concatNev(b) diff --git a/core/src/main/scala/cats/instances/option.scala b/core/src/main/scala/cats/instances/option.scala index 11250ee95a..cc4961103e 100644 --- a/core/src/main/scala/cats/instances/option.scala +++ b/core/src/main/scala/cats/instances/option.scala @@ -12,8 +12,12 @@ trait OptionInstances extends cats.kernel.instances.OptionInstances { with CommutativeMonad[Option] with CoflatMap[Option] with Align[Option] = - new Traverse[Option] with MonadError[Option, Unit] with Alternative[Option] with CommutativeMonad[Option] - with CoflatMap[Option] with Align[Option] { + new Traverse[Option] + with MonadError[Option, Unit] + with Alternative[Option] + with CommutativeMonad[Option] + with CoflatMap[Option] + with Align[Option] { def empty[A]: Option[A] = None From e249b9613c3385cc963fd7fc34fcd0cedf693a03 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Tue, 15 Oct 2019 10:33:48 -0400 Subject: [PATCH 6/8] Use lazy iterator for align --- .../cats/data/NonEmptyLazyList.scala | 44 +++++++++---------- .../scala-2.13+/cats/instances/lazyList.scala | 31 +++++++------ .../cats/tests/NonEmptyLazyListSuite.scala | 2 +- 3 files changed, 40 insertions(+), 37 deletions(-) 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 c4cd5de0e9..3906f94315 100644 --- a/core/src/main/scala-2.13+/cats/data/NonEmptyLazyList.scala +++ b/core/src/main/scala-2.13+/cats/data/NonEmptyLazyList.scala @@ -334,28 +334,28 @@ sealed abstract private[data] class NonEmptyLazyListInstances extends NonEmptyLa 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)) - }) - } + 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 053bbb18c8..bf570f606a 100644 --- a/core/src/main/scala-2.13+/cats/instances/lazyList.scala +++ b/core/src/main/scala-2.13+/cats/instances/lazyList.scala @@ -6,15 +6,17 @@ import cats.syntax.show._ import cats.data.Ior import cats.data.ZipLazyList - 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] with Align[LazyList] = - new 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 @@ -134,19 +136,20 @@ trait LazyListInstances extends cats.kernel.instances.LazyListInstances { 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) + val alignIterator = new Iterator[C] { + val iterA = fa.iterator + val iterB = fb.iterator + def hasNext: Boolean = iterA.hasNext || iterB.hasNext + def next(): C = + f( + 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 + + LazyList.from(alignIterator) } } 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 36a81c9bef..2468af0aa3 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.{AlignTests,BimonadTests, NonEmptyTraverseTests, SemigroupKTests, SerializableTests} +import cats.laws.discipline.{AlignTests, BimonadTests, NonEmptyTraverseTests, SemigroupKTests, SerializableTests} import cats.laws.discipline.arbitrary._ class NonEmptyLazyListSuite extends CatsSuite { From 6e7d2b449a23b9fab07077fab6b565864cb858fe Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Tue, 15 Oct 2019 13:45:09 -0400 Subject: [PATCH 7/8] Add doctests and Either tests --- core/src/main/scala/cats/Align.scala | 36 ++++++++++++++++++- .../test/scala/cats/tests/EitherSuite.scala | 4 +++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/cats/Align.scala b/core/src/main/scala/cats/Align.scala index 884bda6ece..4100a4a419 100644 --- a/core/src/main/scala/cats/Align.scala +++ b/core/src/main/scala/cats/Align.scala @@ -17,30 +17,64 @@ import cats.data.Ior /** * 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)) + * Example: + * {{{ + * scala> import cats.implicits._ + * scala> import cats.data.Ior + * scala> Align[List].align(List(1, 2), List(10, 11, 12)) + * res0: List[Ior[Int, Int]] = List(Both(1,10), Both(2,11), 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. + * + * Example: + * {{{ + * scala> import cats.implicits._ + * scala> Align[List].alignWith(List(1, 2), List(10, 11, 12))(_.mergeLeft) + * res0: List[Int] = List(1, 2, 12) + * }}} */ 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. + * + * Example: + * {{{ + * scala> import cats.implicits._ + * scala> Align[List].alignCombine(List(1, 2), List(10, 11, 12)) + * res0: List[Int] = List(11, 13, 12) + * }}} */ 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. + * + * Example: + * {{{ + * scala> import cats.implicits._ + * scala> Align[List].padZip(List(1, 2), List(10)) + * res0: List[(Option[Int], Option[Int])] = List((Some(1),Some(10)), (Some(2),None)) + * }}} */ 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. + * + * Example: + * {{{ + * scala> import cats.implicits._ + * scala> Align[List].padZipWith(List(1, 2), List(10, 11, 12))(_ |+| _) + * res0: List[Option[Int]] = List(Some(11), Some(13), Some(12)) + * }}} */ 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/tests/src/test/scala/cats/tests/EitherSuite.scala b/tests/src/test/scala/cats/tests/EitherSuite.scala index 3544434b71..3d6007516d 100644 --- a/tests/src/test/scala/cats/tests/EitherSuite.scala +++ b/tests/src/test/scala/cats/tests/EitherSuite.scala @@ -5,6 +5,7 @@ import cats.data.{EitherT, NonEmptyChain, NonEmptyList, NonEmptySet, Validated} import cats.laws.discipline._ import cats.kernel.laws.discipline.{EqTests, MonoidTests, OrderTests, PartialOrderTests, SemigroupTests} import org.scalatest.funsuite.AnyFunSuiteLike +import cats.laws.discipline.arbitrary._ import scala.util.Try @@ -17,6 +18,9 @@ class EitherSuite extends CatsSuite { checkAll("Either[Int, Int]", SemigroupalTests[Either[Int, *]].semigroupal[Int, Int, Int]) checkAll("Semigroupal[Either[Int, *]]", SerializableTests.serializable(Semigroupal[Either[Int, *]])) + checkAll("Either[Int, Int]", AlignTests[Either[Int, *]].align[Int, Int, Int, Int]) + checkAll("Align[Either[Int, *]]", SerializableTests.serializable(Align[Either[Int, *]])) + implicit val eq0 = EitherT.catsDataEqForEitherT[Either[Int, *], Int, Int] checkAll("Either[Int, Int]", MonadErrorTests[Either[Int, *], Int].monadError[Int, Int, Int]) From 5be6a8607eeb1459f225690b573187397ae128e9 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Mon, 4 Nov 2019 13:45:57 -0500 Subject: [PATCH 8/8] Address feedback --- build.sbt | 3 --- .../scala-2.13+/cats/data/NonEmptyLazyList.scala | 13 ++++++++++++- core/src/main/scala/cats/Align.scala | 7 +++++-- .../cats/data/AbstractNonEmptyInstances.scala | 15 ++------------- core/src/main/scala/cats/data/NonEmptyChain.scala | 12 +++++++++++- 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/build.sbt b/build.sbt index 281de4a830..38f08cb4be 100644 --- a/build.sbt +++ b/build.sbt @@ -387,9 +387,6 @@ def mimaSettings(moduleName: String) = exclude[MissingClassProblem]( "cats.kernel.compat.scalaVersionMoreSpecific$suppressUnusedImportWarningForScalaVersionMoreSpecific" ) - ) ++ //abstract package private classes - Seq( - exclude[DirectMissingMethodProblem]("cats.data.AbstractNonEmptyInstances.this") ) ++ // Only narrowing of types allowed here Seq( exclude[IncompatibleSignatureProblem]("*") 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 3906f94315..6f11f312f7 100644 --- a/core/src/main/scala-2.13+/cats/data/NonEmptyLazyList.scala +++ b/core/src/main/scala-2.13+/cats/data/NonEmptyLazyList.scala @@ -334,7 +334,7 @@ sealed abstract private[data] class NonEmptyLazyListInstances extends NonEmptyLa with NonEmptyTraverse[NonEmptyLazyList] with SemigroupK[NonEmptyLazyList] with Align[NonEmptyLazyList] = - new AbstractNonEmptyInstances[LazyList, NonEmptyLazyList] { + new AbstractNonEmptyInstances[LazyList, NonEmptyLazyList] with Align[NonEmptyLazyList] { def extract[A](fa: NonEmptyLazyList[A]): A = fa.head @@ -355,6 +355,17 @@ sealed abstract private[data] class NonEmptyLazyListInstances extends NonEmptyLa Eval.defer(fa.reduceRightTo(a => Eval.now(f(a))) { (a, b) => Eval.defer(g(a, b)) }) + + private val alignInstance = Align[LazyList].asInstanceOf[Align[NonEmptyLazyList]] + + def functor: Functor[NonEmptyLazyList] = alignInstance.functor + + def align[A, B](fa: NonEmptyLazyList[A], fb: NonEmptyLazyList[B]): NonEmptyLazyList[Ior[A, B]] = + alignInstance.align(fa, fb) + + override def alignWith[A, B, C](fa: NonEmptyLazyList[A], + fb: NonEmptyLazyList[B])(f: Ior[A, B] => C): NonEmptyLazyList[C] = + alignInstance.alignWith(fa, fb)(f) } implicit def catsDataOrderForNonEmptyLazyList[A: Order]: Order[NonEmptyLazyList[A]] = diff --git a/core/src/main/scala/cats/Align.scala b/core/src/main/scala/cats/Align.scala index 4100a4a419..801d15a65c 100644 --- a/core/src/main/scala/cats/Align.scala +++ b/core/src/main/scala/cats/Align.scala @@ -77,11 +77,14 @@ import cats.data.Ior * }}} */ 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)) + alignWith(fa, fb) { ior => + val (oa, ob) = ior.pad + f(oa, ob) + } } object Align { - def semigroup[F[_]: Align, A: Semigroup]: Semigroup[F[A]] = new Semigroup[F[A]] { + def semigroup[F[_], A](implicit F: Align[F], A: Semigroup[A]): Semigroup[F[A]] = new Semigroup[F[A]] { def combine(x: F[A], y: F[A]): F[A] = Align[F].alignCombine(x, y) } } diff --git a/core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala b/core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala index e343e64509..2641a4f364 100644 --- a/core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala +++ b/core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala @@ -4,17 +4,14 @@ package data abstract private[data] class AbstractNonEmptyInstances[F[_], NonEmptyF[_]](implicit MF: Monad[F], CF: CoflatMap[F], TF: Traverse[F], - SF: SemigroupK[F], - AF: Align[F]) + SF: SemigroupK[F]) extends Bimonad[NonEmptyF] with NonEmptyTraverse[NonEmptyF] - with SemigroupK[NonEmptyF] - with Align[NonEmptyF] { + with SemigroupK[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) @@ -80,12 +77,4 @@ 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/NonEmptyChain.scala b/core/src/main/scala/cats/data/NonEmptyChain.scala index 69a232e719..45d5dbf4db 100644 --- a/core/src/main/scala/cats/data/NonEmptyChain.scala +++ b/core/src/main/scala/cats/data/NonEmptyChain.scala @@ -422,7 +422,7 @@ sealed abstract private[data] class NonEmptyChainInstances extends NonEmptyChain with NonEmptyTraverse[NonEmptyChain] with Bimonad[NonEmptyChain] with Align[NonEmptyChain] = - new AbstractNonEmptyInstances[Chain, NonEmptyChain] { + new AbstractNonEmptyInstances[Chain, NonEmptyChain] with Align[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]] = @@ -453,6 +453,16 @@ sealed abstract private[data] class NonEmptyChainInstances extends NonEmptyChain override def get[A](fa: NonEmptyChain[A])(idx: Long): Option[A] = if (idx == 0) Some(fa.head) else fa.tail.get(idx - 1) + + private val alignInstance = Align[Chain].asInstanceOf[Align[NonEmptyChain]] + + def functor: Functor[NonEmptyChain] = alignInstance.functor + + def align[A, B](fa: NonEmptyChain[A], fb: NonEmptyChain[B]): NonEmptyChain[Ior[A, B]] = + alignInstance.align(fa, fb) + + override def alignWith[A, B, C](fa: NonEmptyChain[A], fb: NonEmptyChain[B])(f: Ior[A, B] => C): NonEmptyChain[C] = + alignInstance.alignWith(fa, fb)(f) } implicit def catsDataOrderForNonEmptyChain[A: Order]: Order[NonEmptyChain[A]] =