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/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] + ) ) } 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..31917aa3bf --- /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) +} 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..6f11f312f7 100644 --- a/core/src/main/scala-2.13+/cats/data/NonEmptyLazyList.scala +++ b/core/src/main/scala-2.13+/cats/data/NonEmptyLazyList.scala @@ -330,9 +330,11 @@ 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] { + implicit val catsDataInstancesForNonEmptyLazyList: Bimonad[NonEmptyLazyList] + with NonEmptyTraverse[NonEmptyLazyList] + with SemigroupK[NonEmptyLazyList] + with Align[NonEmptyLazyList] = + new AbstractNonEmptyInstances[LazyList, NonEmptyLazyList] with Align[NonEmptyLazyList] { def extract[A](fa: NonEmptyLazyList[A]): A = fa.head @@ -353,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-2.13+/cats/instances/lazyList.scala b/core/src/main/scala-2.13+/cats/instances/lazyList.scala index 9e9c344a92..bf570f606a 100644 --- a/core/src/main/scala-2.13+/cats/instances/lazyList.scala +++ b/core/src/main/scala-2.13+/cats/instances/lazyList.scala @@ -3,14 +3,20 @@ package instances import cats.kernel 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] = - 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 @@ -123,6 +129,28 @@ 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 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()) + ) + } + + LazyList.from(alignIterator) + } } 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..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/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/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) - } 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 442ddd9806..26931c1840 100644 --- a/core/src/main/scala/cats/data/Validated.scala +++ b/core/src/main/scala/cats/data/Validated.scala @@ -835,6 +835,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 ff2025a146..18ba15f413 100644 --- a/core/src/main/scala/cats/instances/either.scala +++ b/core/src/main/scala/cats/instances/either.scala @@ -7,6 +7,7 @@ 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] = @@ -33,8 +34,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] = @@ -142,6 +144,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 d93cadfafe..0e7fcde6cd 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 4ac4721cfb..76b23e638e 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 107efceca8..6f29488f8d 100644 --- a/core/src/main/scala/cats/instances/vector.scala +++ b/core/src/main/scala/cats/instances/vector.scala @@ -7,11 +7,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] @@ -110,6 +111,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..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-2.13+/cats/tests/LazyListSuite.scala b/tests/src/test/scala-2.13+/cats/tests/LazyListSuite.scala index b8d56f6f10..3da64533d3 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, @@ -34,6 +35,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..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.{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/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/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/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 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]) diff --git a/tests/src/test/scala/cats/tests/ListSuite.scala b/tests/src/test/scala/cats/tests/ListSuite.scala index 6dce75d59d..9befc591b2 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, @@ -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 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/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 89cd47b60e..51289748d7 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") {