Skip to content

Commit 32a9e08

Browse files
authored
Add Align typeclass (typelevel#3076)
* Add Align typeclass * Scalafmt * Add Const and Validated instances and derivation from SemigroupK and Apply * Add MiMaException tests * scalafmt * Use lazy iterator for align * Add doctests and Either tests * Address feedback
1 parent 134570b commit 32a9e08

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+620
-35
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ TAGS
1717
.idea/*
1818
.idea_modules
1919
.DS_Store
20+
.vscode
2021
.sbtrc
2122
*.sublime-project
2223
*.sublime-workspace

binCompatTest/src/main/scala/catsBC/MimaExceptions.scala

+6-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ object MimaExceptions {
2929
Either.catchOnly[NumberFormatException] { "foo".toInt },
3030
(1.validNel[String], 2.validNel[String], 3.validNel[String]) mapN (_ + _ + _),
3131
(1.asRight[String], 2.asRight[String], 3.asRight[String]) parMapN (_ + _ + _),
32-
InjectK.catsReflexiveInjectKInstance[Option]
32+
InjectK.catsReflexiveInjectKInstance[Option],
33+
(
34+
cats.Bimonad[cats.data.NonEmptyChain],
35+
cats.NonEmptyTraverse[cats.data.NonEmptyChain],
36+
cats.SemigroupK[cats.data.NonEmptyChain]
37+
)
3338
)
3439
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package cats.compat
2+
3+
private[cats] object Vector {
4+
def zipWith[A, B, C](fa: Vector[A], fb: Vector[B])(f: (A, B) => C): Vector[C] =
5+
(fa, fb).zipped.map(f)
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package cats.compat
2+
3+
private[cats] object Vector {
4+
def zipWith[A, B, C](fa: Vector[A], fb: Vector[B])(f: (A, B) => C): Vector[C] =
5+
fa.lazyZip(fb).map(f)
6+
}

core/src/main/scala-2.13+/cats/data/NonEmptyLazyList.scala

+16-3
Original file line numberDiff line numberDiff line change
@@ -330,9 +330,11 @@ class NonEmptyLazyListOps[A](private val value: NonEmptyLazyList[A]) extends Any
330330

331331
sealed abstract private[data] class NonEmptyLazyListInstances extends NonEmptyLazyListInstances1 {
332332

333-
implicit val catsDataInstancesForNonEmptyLazyList
334-
: Bimonad[NonEmptyLazyList] with NonEmptyTraverse[NonEmptyLazyList] with SemigroupK[NonEmptyLazyList] =
335-
new AbstractNonEmptyInstances[LazyList, NonEmptyLazyList] {
333+
implicit val catsDataInstancesForNonEmptyLazyList: Bimonad[NonEmptyLazyList]
334+
with NonEmptyTraverse[NonEmptyLazyList]
335+
with SemigroupK[NonEmptyLazyList]
336+
with Align[NonEmptyLazyList] =
337+
new AbstractNonEmptyInstances[LazyList, NonEmptyLazyList] with Align[NonEmptyLazyList] {
336338

337339
def extract[A](fa: NonEmptyLazyList[A]): A = fa.head
338340

@@ -353,6 +355,17 @@ sealed abstract private[data] class NonEmptyLazyListInstances extends NonEmptyLa
353355
Eval.defer(fa.reduceRightTo(a => Eval.now(f(a))) { (a, b) =>
354356
Eval.defer(g(a, b))
355357
})
358+
359+
private val alignInstance = Align[LazyList].asInstanceOf[Align[NonEmptyLazyList]]
360+
361+
def functor: Functor[NonEmptyLazyList] = alignInstance.functor
362+
363+
def align[A, B](fa: NonEmptyLazyList[A], fb: NonEmptyLazyList[B]): NonEmptyLazyList[Ior[A, B]] =
364+
alignInstance.align(fa, fb)
365+
366+
override def alignWith[A, B, C](fa: NonEmptyLazyList[A],
367+
fb: NonEmptyLazyList[B])(f: Ior[A, B] => C): NonEmptyLazyList[C] =
368+
alignInstance.alignWith(fa, fb)(f)
356369
}
357370

358371
implicit def catsDataOrderForNonEmptyLazyList[A: Order]: Order[NonEmptyLazyList[A]] =

core/src/main/scala-2.13+/cats/instances/lazyList.scala

+30-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@ package instances
33

44
import cats.kernel
55
import cats.syntax.show._
6+
import cats.data.Ior
67
import cats.data.ZipLazyList
78

89
import scala.annotation.tailrec
910

1011
trait LazyListInstances extends cats.kernel.instances.LazyListInstances {
12+
1113
implicit val catsStdInstancesForLazyList
12-
: Traverse[LazyList] with Alternative[LazyList] with Monad[LazyList] with CoflatMap[LazyList] =
13-
new Traverse[LazyList] with Alternative[LazyList] with Monad[LazyList] with CoflatMap[LazyList] {
14+
: Traverse[LazyList] with Alternative[LazyList] with Monad[LazyList] with CoflatMap[LazyList] with Align[LazyList] =
15+
new Traverse[LazyList]
16+
with Alternative[LazyList]
17+
with Monad[LazyList]
18+
with CoflatMap[LazyList]
19+
with Align[LazyList] {
1420

1521
def empty[A]: LazyList[A] = LazyList.empty
1622

@@ -123,6 +129,28 @@ trait LazyListInstances extends cats.kernel.instances.LazyListInstances {
123129

124130
override def collectFirstSome[A, B](fa: LazyList[A])(f: A => Option[B]): Option[B] =
125131
fa.collectFirst(Function.unlift(f))
132+
133+
def functor: Functor[LazyList] = this
134+
135+
def align[A, B](fa: LazyList[A], fb: LazyList[B]): LazyList[Ior[A, B]] =
136+
alignWith(fa, fb)(identity)
137+
138+
override def alignWith[A, B, C](fa: LazyList[A], fb: LazyList[B])(f: Ior[A, B] => C): LazyList[C] = {
139+
140+
val alignIterator = new Iterator[C] {
141+
val iterA = fa.iterator
142+
val iterB = fb.iterator
143+
def hasNext: Boolean = iterA.hasNext || iterB.hasNext
144+
def next(): C =
145+
f(
146+
if (iterA.hasNext && iterB.hasNext) Ior.both(iterA.next(), iterB.next())
147+
else if (iterA.hasNext) Ior.left(iterA.next())
148+
else Ior.right(iterB.next())
149+
)
150+
}
151+
152+
LazyList.from(alignIterator)
153+
}
126154
}
127155

128156
implicit def catsStdShowForLazyList[A: Show]: Show[LazyList[A]] =

core/src/main/scala/cats/Align.scala

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package cats
2+
3+
import simulacrum.typeclass
4+
5+
import cats.data.Ior
6+
7+
/**
8+
* `Align` supports zipping together structures with different shapes,
9+
* holding the results from either or both structures in an `Ior`.
10+
*
11+
* Must obey the laws in cats.laws.AlignLaws
12+
*/
13+
@typeclass trait Align[F[_]] {
14+
15+
def functor: Functor[F]
16+
17+
/**
18+
* Pairs elements of two structures along the union of their shapes, using `Ior` to hold the results.
19+
*
20+
* Example:
21+
* {{{
22+
* scala> import cats.implicits._
23+
* scala> import cats.data.Ior
24+
* scala> Align[List].align(List(1, 2), List(10, 11, 12))
25+
* res0: List[Ior[Int, Int]] = List(Both(1,10), Both(2,11), Right(12))
26+
* }}}
27+
*/
28+
def align[A, B](fa: F[A], fb: F[B]): F[Ior[A, B]]
29+
30+
/**
31+
* Combines elements similarly to `align`, using the provided function to compute the results.
32+
*
33+
* Example:
34+
* {{{
35+
* scala> import cats.implicits._
36+
* scala> Align[List].alignWith(List(1, 2), List(10, 11, 12))(_.mergeLeft)
37+
* res0: List[Int] = List(1, 2, 12)
38+
* }}}
39+
*/
40+
def alignWith[A, B, C](fa: F[A], fb: F[B])(f: Ior[A, B] => C): F[C] =
41+
functor.map(align(fa, fb))(f)
42+
43+
/**
44+
* Align two structures with the same element, combining results according to their semigroup instances.
45+
*
46+
* Example:
47+
* {{{
48+
* scala> import cats.implicits._
49+
* scala> Align[List].alignCombine(List(1, 2), List(10, 11, 12))
50+
* res0: List[Int] = List(11, 13, 12)
51+
* }}}
52+
*/
53+
def alignCombine[A: Semigroup](fa1: F[A], fa2: F[A]): F[A] =
54+
alignWith(fa1, fa2)(_.merge)
55+
56+
/**
57+
* Same as `align`, but forgets from the type that one of the two elements must be present.
58+
*
59+
* Example:
60+
* {{{
61+
* scala> import cats.implicits._
62+
* scala> Align[List].padZip(List(1, 2), List(10))
63+
* res0: List[(Option[Int], Option[Int])] = List((Some(1),Some(10)), (Some(2),None))
64+
* }}}
65+
*/
66+
def padZip[A, B](fa: F[A], fb: F[B]): F[(Option[A], Option[B])] =
67+
alignWith(fa, fb)(_.pad)
68+
69+
/**
70+
* Same as `alignWith`, but forgets from the type that one of the two elements must be present.
71+
*
72+
* Example:
73+
* {{{
74+
* scala> import cats.implicits._
75+
* scala> Align[List].padZipWith(List(1, 2), List(10, 11, 12))(_ |+| _)
76+
* res0: List[Option[Int]] = List(Some(11), Some(13), Some(12))
77+
* }}}
78+
*/
79+
def padZipWith[A, B, C](fa: F[A], fb: F[B])(f: (Option[A], Option[B]) => C): F[C] =
80+
alignWith(fa, fb) { ior =>
81+
val (oa, ob) = ior.pad
82+
f(oa, ob)
83+
}
84+
}
85+
86+
object Align {
87+
def semigroup[F[_], A](implicit F: Align[F], A: Semigroup[A]): Semigroup[F[A]] = new Semigroup[F[A]] {
88+
def combine(x: F[A], y: F[A]): F[A] = Align[F].alignCombine(x, y)
89+
}
90+
}

core/src/main/scala/cats/Apply.scala

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cats
22

33
import simulacrum.typeclass
44
import simulacrum.noop
5+
import cats.data.Ior
56

67
/**
78
* Weaker version of Applicative[F]; has apply but not pure.
@@ -225,6 +226,11 @@ object Apply {
225226
*/
226227
def semigroup[F[_], A](implicit f: Apply[F], sg: Semigroup[A]): Semigroup[F[A]] =
227228
new ApplySemigroup[F, A](f, sg)
229+
230+
def align[F[_]: Apply]: Align[F] = new Align[F] {
231+
def align[A, B](fa: F[A], fb: F[B]): F[Ior[A, B]] = Apply[F].map2(fa, fb)(Ior.both)
232+
def functor: Functor[F] = Apply[F]
233+
}
228234
}
229235

230236
private[cats] class ApplySemigroup[F[_], A](f: Apply[F], sg: Semigroup[A]) extends Semigroup[F[A]] {

core/src/main/scala/cats/SemigroupK.scala

+9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cats
22

33
import simulacrum.typeclass
4+
import cats.data.Ior
45

56
/**
67
* SemigroupK is a universal semigroup which operates on kinds.
@@ -68,3 +69,11 @@ import simulacrum.typeclass
6869
val F = self
6970
}
7071
}
72+
73+
object SemigroupK {
74+
def align[F[_]: SemigroupK: Functor]: Align[F] = new Align[F] {
75+
def align[A, B](fa: F[A], fb: F[B]): F[Ior[A, B]] =
76+
SemigroupK[F].combineK(Functor[F].map(fa)(Ior.left), Functor[F].map(fb)(Ior.right))
77+
def functor: Functor[F] = Functor[F]
78+
}
79+
}

core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala

-1
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,4 @@ abstract private[data] class AbstractNonEmptyInstances[F[_], NonEmptyF[_]](impli
7777

7878
override def collectFirstSome[A, B](fa: NonEmptyF[A])(f: A => Option[B]): Option[B] =
7979
traverseInstance.collectFirstSome(fa)(f)
80-
8180
}

core/src/main/scala/cats/data/Chain.scala

+23-2
Original file line numberDiff line numberDiff line change
@@ -681,8 +681,8 @@ sealed abstract private[data] class ChainInstances extends ChainInstances1 {
681681
}
682682

683683
implicit val catsDataInstancesForChain
684-
: Traverse[Chain] with Alternative[Chain] with Monad[Chain] with CoflatMap[Chain] =
685-
new Traverse[Chain] with Alternative[Chain] with Monad[Chain] with CoflatMap[Chain] {
684+
: Traverse[Chain] with Alternative[Chain] with Monad[Chain] with CoflatMap[Chain] with Align[Chain] =
685+
new Traverse[Chain] with Alternative[Chain] with Monad[Chain] with CoflatMap[Chain] with Align[Chain] {
686686
def foldLeft[A, B](fa: Chain[A], b: B)(f: (B, A) => B): B =
687687
fa.foldLeft(b)(f)
688688
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 {
743743
}
744744

745745
override def get[A](fa: Chain[A])(idx: Long): Option[A] = fa.get(idx)
746+
747+
def functor: Functor[Chain] = this
748+
749+
def align[A, B](fa: Chain[A], fb: Chain[B]): Chain[Ior[A, B]] =
750+
alignWith(fa, fb)(identity)
751+
752+
override def alignWith[A, B, C](fa: Chain[A], fb: Chain[B])(f: Ior[A, B] => C): Chain[C] = {
753+
val iterA = fa.iterator
754+
val iterB = fb.iterator
755+
756+
var result: Chain[C] = Chain.empty
757+
758+
while (iterA.hasNext || iterB.hasNext) {
759+
val ior =
760+
if (iterA.hasNext && iterB.hasNext) Ior.both(iterA.next(), iterB.next())
761+
else if (iterA.hasNext) Ior.left(iterA.next())
762+
else Ior.right(iterB.next())
763+
result = result :+ f(ior)
764+
}
765+
result
766+
}
746767
}
747768

748769
implicit def catsDataShowForChain[A](implicit A: Show[A]): Show[Chain[A]] =

core/src/main/scala/cats/data/Const.scala

+6
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ sealed abstract private[data] class ConstInstances extends ConstInstances0 {
7474
x.compare(y)
7575
}
7676

77+
implicit def catsDataAlignForConst[A: Semigroup]: Align[Const[A, *]] = new Align[Const[A, *]] {
78+
def align[B, C](fa: Const[A, B], fb: Const[A, C]): Const[A, Ior[B, C]] =
79+
Const(Semigroup[A].combine(fa.getConst, fb.getConst))
80+
def functor: Functor[Const[A, *]] = catsDataFunctorForConst
81+
}
82+
7783
implicit def catsDataShowForConst[A: Show, B]: Show[Const[A, B]] = new Show[Const[A, B]] {
7884
def show(f: Const[A, B]): String = f.show
7985
}

core/src/main/scala/cats/data/NonEmptyChain.scala

+15-3
Original file line numberDiff line numberDiff line change
@@ -418,9 +418,11 @@ class NonEmptyChainOps[A](private val value: NonEmptyChain[A]) extends AnyVal {
418418

419419
sealed abstract private[data] class NonEmptyChainInstances extends NonEmptyChainInstances1 {
420420

421-
implicit val catsDataInstancesForNonEmptyChain
422-
: SemigroupK[NonEmptyChain] with NonEmptyTraverse[NonEmptyChain] with Bimonad[NonEmptyChain] =
423-
new AbstractNonEmptyInstances[Chain, NonEmptyChain] {
421+
implicit val catsDataInstancesForNonEmptyChain: SemigroupK[NonEmptyChain]
422+
with NonEmptyTraverse[NonEmptyChain]
423+
with Bimonad[NonEmptyChain]
424+
with Align[NonEmptyChain] =
425+
new AbstractNonEmptyInstances[Chain, NonEmptyChain] with Align[NonEmptyChain] {
424426
def extract[A](fa: NonEmptyChain[A]): A = fa.head
425427

426428
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
451453

452454
override def get[A](fa: NonEmptyChain[A])(idx: Long): Option[A] =
453455
if (idx == 0) Some(fa.head) else fa.tail.get(idx - 1)
456+
457+
private val alignInstance = Align[Chain].asInstanceOf[Align[NonEmptyChain]]
458+
459+
def functor: Functor[NonEmptyChain] = alignInstance.functor
460+
461+
def align[A, B](fa: NonEmptyChain[A], fb: NonEmptyChain[B]): NonEmptyChain[Ior[A, B]] =
462+
alignInstance.align(fa, fb)
463+
464+
override def alignWith[A, B, C](fa: NonEmptyChain[A], fb: NonEmptyChain[B])(f: Ior[A, B] => C): NonEmptyChain[C] =
465+
alignInstance.alignWith(fa, fb)(f)
454466
}
455467

456468
implicit def catsDataOrderForNonEmptyChain[A: Order]: Order[NonEmptyChain[A]] =

core/src/main/scala/cats/data/NonEmptyList.scala

+22-2
Original file line numberDiff line numberDiff line change
@@ -510,11 +510,12 @@ object NonEmptyList extends NonEmptyListInstances {
510510
sealed abstract private[data] class NonEmptyListInstances extends NonEmptyListInstances0 {
511511

512512
implicit val catsDataInstancesForNonEmptyList
513-
: SemigroupK[NonEmptyList] with Bimonad[NonEmptyList] with NonEmptyTraverse[NonEmptyList] =
513+
: SemigroupK[NonEmptyList] with Bimonad[NonEmptyList] with NonEmptyTraverse[NonEmptyList] with Align[NonEmptyList] =
514514
new NonEmptyReducible[NonEmptyList, List]
515515
with SemigroupK[NonEmptyList]
516516
with Bimonad[NonEmptyList]
517-
with NonEmptyTraverse[NonEmptyList] {
517+
with NonEmptyTraverse[NonEmptyList]
518+
with Align[NonEmptyList] {
518519

519520
def combineK[A](a: NonEmptyList[A], b: NonEmptyList[A]): NonEmptyList[A] =
520521
a.concatNel(b)
@@ -619,6 +620,25 @@ sealed abstract private[data] class NonEmptyListInstances extends NonEmptyListIn
619620

620621
override def get[A](fa: NonEmptyList[A])(idx: Long): Option[A] =
621622
if (idx == 0) Some(fa.head) else Foldable[List].get(fa.tail)(idx - 1)
623+
624+
def functor: Functor[NonEmptyList] = this
625+
626+
def align[A, B](fa: NonEmptyList[A], fb: NonEmptyList[B]): NonEmptyList[Ior[A, B]] =
627+
alignWith(fa, fb)(identity)
628+
629+
override def alignWith[A, B, C](fa: NonEmptyList[A], fb: NonEmptyList[B])(f: Ior[A, B] => C): NonEmptyList[C] = {
630+
631+
@tailrec
632+
def go(as: List[A], bs: List[B], acc: List[C]): List[C] = (as, bs) match {
633+
case (Nil, Nil) => acc
634+
case (Nil, y :: ys) => go(Nil, ys, f(Ior.right(y)) :: acc)
635+
case (x :: xs, Nil) => go(xs, Nil, f(Ior.left(x)) :: acc)
636+
case (x :: xs, y :: ys) => go(xs, ys, f(Ior.both(x, y)) :: acc)
637+
}
638+
639+
NonEmptyList(f(Ior.both(fa.head, fb.head)), go(fa.tail, fb.tail, Nil).reverse)
640+
}
641+
622642
}
623643

624644
implicit def catsDataShowForNonEmptyList[A](implicit A: Show[A]): Show[NonEmptyList[A]] =

0 commit comments

Comments
 (0)