Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Align typeclass #3076

Merged
merged 11 commits into from
Nov 5, 2019
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ TAGS
.idea/*
.idea_modules
.DS_Store
.vscode
.sbtrc
*.sublime-project
*.sublime-workspace
Expand Down
7 changes: 6 additions & 1 deletion binCompatTest/src/main/scala/catsBC/MimaExceptions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
)
)
}
6 changes: 6 additions & 0 deletions core/src/main/scala-2.12/cats/compat/Vector.scala
Original file line number Diff line number Diff line change
@@ -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)
}
6 changes: 6 additions & 0 deletions core/src/main/scala-2.13+/cats/compat/Vector.scala
Original file line number Diff line number Diff line change
@@ -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)
}
19 changes: 16 additions & 3 deletions core/src/main/scala-2.13+/cats/data/NonEmptyLazyList.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]] =
Expand Down
32 changes: 30 additions & 2 deletions core/src/main/scala-2.13+/cats/instances/lazyList.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]] =
Expand Down
90 changes: 90 additions & 0 deletions core/src/main/scala/cats/Align.scala
Original file line number Diff line number Diff line change
@@ -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]
LukaJCB marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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)
}
}
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/Apply.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]] {
Expand Down
9 changes: 9 additions & 0 deletions core/src/main/scala/cats/SemigroupK.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cats

import simulacrum.typeclass
import cats.data.Ior

/**
* SemigroupK is a universal semigroup which operates on kinds.
Expand Down Expand Up @@ -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]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

}
25 changes: 23 additions & 2 deletions core/src/main/scala/cats/data/Chain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down Expand Up @@ -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]] =
Expand Down
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/data/Const.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
18 changes: 15 additions & 3 deletions core/src/main/scala/cats/data/NonEmptyChain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]] =
Expand Down Expand Up @@ -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]] =
Expand Down
24 changes: 22 additions & 2 deletions core/src/main/scala/cats/data/NonEmptyList.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]] =
Expand Down
Loading