From cd10e01e904beaf0668c2e1e8b13c9d1c31835a7 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Tue, 5 Nov 2019 10:54:57 +0100 Subject: [PATCH 1/4] backporting #3076 Unrelated changes --- .gitignore | 1 + core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) 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/core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala b/core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala index 57a65ecf42..2641a4f364 100644 --- a/core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala +++ b/core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala @@ -77,5 +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) - } From 5999702856730530ce022e7c4f67ea89aa64a93e Mon Sep 17 00:00:00 2001 From: Kalra Date: Sun, 8 Mar 2020 17:51:56 +0800 Subject: [PATCH 2/4] backporting #3076 Added Aligh typeclass --- core/src/main/scala/cats/Align.scala | 90 +++++++++++++++++++ core/src/main/scala/cats/syntax/align.scala | 4 + laws/src/main/scala/cats/laws/AlignLaws.scala | 47 ++++++++++ .../cats/laws/discipline/AlignTests.scala | 46 ++++++++++ .../test/scala/cats/tests/AlignSuite.scala | 14 +++ 5 files changed, 201 insertions(+) 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 create mode 100644 tests/src/test/scala/cats/tests/AlignSuite.scala diff --git a/core/src/main/scala/cats/Align.scala b/core/src/main/scala/cats/Align.scala new file mode 100644 index 0000000000..801d15a65c --- /dev/null +++ b/core/src/main/scala/cats/Align.scala @@ -0,0 +1,90 @@ +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. + * + * 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 => + val (oa, ob) = ior.pad + f(oa, ob) + } +} + +object Align { + 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/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/laws/src/main/scala/cats/laws/AlignLaws.scala b/laws/src/main/scala/cats/laws/AlignLaws.scala new file mode 100644 index 0000000000..590a11fb79 --- /dev/null +++ b/laws/src/main/scala/cats/laws/AlignLaws.scala @@ -0,0 +1,47 @@ +package cats +package laws + +import cats.syntax.align._ +import cats.syntax.functor._ + +import cats.data.Ior +import cats.data.Ior.{Both, Left, Right} + +/** + * 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 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]] = + 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..8d35bb3acf --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/AlignTests.scala @@ -0,0 +1,46 @@ +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 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/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) + } +} From e2c468b1b60192e2756ac38a7d7c51f0b02901d8 Mon Sep 17 00:00:00 2001 From: Kalra Date: Sun, 8 Mar 2020 18:10:21 +0800 Subject: [PATCH 3/4] backporting #3076 Added Align typeclass --- core/src/main/scala/cats/syntax/all.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index 3c1fcfc40c..da63679486 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -99,3 +99,4 @@ trait AllSyntaxBinCompat7 extends FunctorSyntaxBinCompat0 with BiFoldableSyntaxBinCompat0 with SemigroupKSyntaxBinCompat0 + with AlignSyntax From 56bb7d092d3d4d77fd6297d0a4b0a509b3c324a6 Mon Sep 17 00:00:00 2001 From: Kalra Date: Sun, 8 Mar 2020 19:18:14 +0800 Subject: [PATCH 4/4] backporting #3076 Added Align instances --- .../main/scala/catsBC/MimaExceptions.scala | 9 +++++- core/src/main/scala/cats/Apply.scala | 6 ++++ core/src/main/scala/cats/SemigroupK.scala | 9 ++++++ core/src/main/scala/cats/data/Chain.scala | 25 ++++++++++++++-- core/src/main/scala/cats/data/Const.scala | 6 ++++ .../main/scala/cats/data/NonEmptyChain.scala | 18 ++++++++++-- .../main/scala/cats/data/NonEmptyList.scala | 24 +++++++++++++-- .../scala/cats/data/NonEmptyMapImpl.scala | 9 ++++-- .../main/scala/cats/data/NonEmptyVector.scala | 18 ++++++++++-- core/src/main/scala/cats/data/Validated.scala | 21 ++++++++++++++ .../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 | 20 +++++++++++-- .../main/scala/cats/instances/sortedMap.scala | 29 +++++++++++++++++-- .../main/scala/cats/instances/vector.scala | 17 +++++++++-- core/src/main/scala/cats/syntax/package.scala | 1 + .../scala/cats/tests/ApplicativeSuite.scala | 10 +++++++ .../test/scala/cats/tests/ChainSuite.scala | 4 +++ .../test/scala/cats/tests/ConstSuite.scala | 3 ++ .../test/scala/cats/tests/EitherSuite.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/SemigroupKSuite.scala | 20 +++++++++++++ .../scala/cats/tests/SortedMapSuite.scala | 4 +++ .../test/scala/cats/tests/SyntaxSuite.scala | 17 +++++++++++ .../scala/cats/tests/ValidatedSuite.scala | 3 ++ .../test/scala/cats/tests/VectorSuite.scala | 4 +++ 32 files changed, 353 insertions(+), 29 deletions(-) create mode 100644 tests/src/test/scala/cats/tests/SemigroupKSuite.scala diff --git a/binCompatTest/src/main/scala/catsBC/MimaExceptions.scala b/binCompatTest/src/main/scala/catsBC/MimaExceptions.scala index 5ff5e33444..ad17ec0eca 100644 --- a/binCompatTest/src/main/scala/catsBC/MimaExceptions.scala +++ b/binCompatTest/src/main/scala/catsBC/MimaExceptions.scala @@ -1,4 +1,5 @@ package catsBC +import cats.InjectK import cats.implicits._ object MimaExceptions { @@ -27,6 +28,12 @@ object MimaExceptions { true.iterateUntilM(Option(_))(identity _), 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 (_ + _ + _) + (1.asRight[String], 2.asRight[String], 3.asRight[String]) parMapN (_ + _ + _), + InjectK.catsReflexiveInjectKInstance[Option], + ( + cats.Bimonad[cats.data.NonEmptyChain], + cats.NonEmptyTraverse[cats.data.NonEmptyChain], + cats.SemigroupK[cats.data.NonEmptyChain] + ) ) } 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/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/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/NonEmptyChain.scala b/core/src/main/scala/cats/data/NonEmptyChain.scala index fc4ed37435..45d5dbf4db 100644 --- a/core/src/main/scala/cats/data/NonEmptyChain.scala +++ b/core/src/main/scala/cats/data/NonEmptyChain.scala @@ -418,9 +418,11 @@ 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] { + implicit val catsDataInstancesForNonEmptyChain: SemigroupK[NonEmptyChain] + with NonEmptyTraverse[NonEmptyChain] + with Bimonad[NonEmptyChain] + with Align[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]] = @@ -451,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]] = diff --git a/core/src/main/scala/cats/data/NonEmptyList.scala b/core/src/main/scala/cats/data/NonEmptyList.scala index 43fb0c7390..9bea4dc4d4 100644 --- a/core/src/main/scala/cats/data/NonEmptyList.scala +++ b/core/src/main/scala/cats/data/NonEmptyList.scala @@ -510,11 +510,12 @@ 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) @@ -619,6 +620,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 606d477ec4..07f2324f75 100644 --- a/core/src/main/scala/cats/data/NonEmptyVector.scala +++ b/core/src/main/scala/cats/data/NonEmptyVector.scala @@ -238,12 +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] = + 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) @@ -360,6 +363,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/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/core/src/main/scala/cats/instances/either.scala b/core/src/main/scala/cats/instances/either.scala index e0363dd72a..5fe4594802 100644 --- a/core/src/main/scala/cats/instances/either.scala +++ b/core/src/main/scala/cats/instances/either.scala @@ -5,6 +5,7 @@ import cats.data.Validated 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] = @@ -31,8 +32,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] = @@ -140,6 +142,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 5dc7803de3..27f38e6c41 100644 --- a/core/src/main/scala/cats/instances/list.scala +++ b/core/src/main/scala/cats/instances/list.scala @@ -7,10 +7,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 @@ -75,6 +78,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 @@ -143,7 +162,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 2de68b1ed0..cc4961103e 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,12 +10,14 @@ 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 @@ -119,6 +122,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 5a5817512d..fc9b61cf90 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 11b54466ac..d3987f9a2a 100644 --- a/core/src/main/scala/cats/instances/vector.scala +++ b/core/src/main/scala/cats/instances/vector.scala @@ -1,7 +1,7 @@ package cats package instances -import cats.data.ZipVector +import cats.data.{Ior, ZipVector} import cats.syntax.show._ import scala.annotation.tailrec @@ -10,8 +10,8 @@ import scala.collection.immutable.VectorBuilder 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] @@ -110,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) { + (fa, fb).zipped.map(Ior.both) ++ fa.drop(fb.size).map(Ior.left) + } else { + (fa, fb).zipped.map(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/package.scala b/core/src/main/scala/cats/syntax/package.scala index d7bba22326..4e443f2be5 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/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/ChainSuite.scala b/tests/src/test/scala/cats/tests/ChainSuite.scala index 4d17d3d7bb..665ae79bea 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/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/EitherSuite.scala b/tests/src/test/scala/cats/tests/EitherSuite.scala index 01d63dcf25..f778992493 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._ import cats.kernel.laws.discipline.{EqTests, MonoidTests, OrderTests, PartialOrderTests, SemigroupTests} import cats.laws.discipline._ 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]) diff --git a/tests/src/test/scala/cats/tests/ListSuite.scala b/tests/src/test/scala/cats/tests/ListSuite.scala index 195286951e..9c1c16a9c8 100644 --- a/tests/src/test/scala/cats/tests/ListSuite.scala +++ b/tests/src/test/scala/cats/tests/ListSuite.scala @@ -4,6 +4,7 @@ package tests import cats.data.{NonEmptyList, ZipList} import cats.laws.discipline.arbitrary._ import cats.laws.discipline.{ + AlignTests, AlternativeTests, CoflatMapTests, CommutativeApplyTests, @@ -35,6 +36,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/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/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 2752af4f14..311bdda5f8 100644 --- a/tests/src/test/scala/cats/tests/SyntaxSuite.scala +++ b/tests/src/test/scala/cats/tests/SyntaxSuite.scala @@ -430,6 +430,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/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]) diff --git a/tests/src/test/scala/cats/tests/VectorSuite.scala b/tests/src/test/scala/cats/tests/VectorSuite.scala index 515b1cf52d..baa0295bb8 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, @@ -34,6 +35,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") {