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
1 change: 1 addition & 0 deletions .gitignore
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
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]
)
)
}
3 changes: 3 additions & 0 deletions build.sbt
Expand Up @@ -387,6 +387,9 @@ def mimaSettings(moduleName: String) =
exclude[MissingClassProblem](
"cats.kernel.compat.scalaVersionMoreSpecific$suppressUnusedImportWarningForScalaVersionMoreSpecific"
)
) ++ //abstract package private classes
Seq(
exclude[DirectMissingMethodProblem]("cats.data.AbstractNonEmptyInstances.this")
Copy link
Contributor

@kailuowang kailuowang Oct 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure this doesn't break BC? If we are sure maybe add a test in binCompatTest? If not can we just add a separate method to provide the instance for Align?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some tests there, I think that should suffice, WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that shall do. thanks!
On a slightly related note, since we have been updating the build, I am a bit nervous that if something breaks binCompatTest, we wouldn't know. I'll create an issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just makes me vaguely uncomfortable. It's only used twice and it seems like it'd be almost equivalently noisy just to write out the new AbstractNonEmptyInstances[F, NEF] with Align[NEF] in those two places.

I know we don't promise bincompat for people using Java, etc., but breaking it for the sake of saving a few lines doesn't feel ideal.

) ++ // Only narrowing of types allowed here
Seq(
exclude[IncompatibleSignatureProblem]("*")
Expand Down
6 changes: 6 additions & 0 deletions 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)
}
6 changes: 6 additions & 0 deletions 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)
}
6 changes: 4 additions & 2 deletions core/src/main/scala-2.13+/cats/data/NonEmptyLazyList.scala
Expand Up @@ -330,8 +330,10 @@ 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] =
implicit val catsDataInstancesForNonEmptyLazyList: Bimonad[NonEmptyLazyList]
with NonEmptyTraverse[NonEmptyLazyList]
with SemigroupK[NonEmptyLazyList]
with Align[NonEmptyLazyList] =
new AbstractNonEmptyInstances[LazyList, NonEmptyLazyList] {

def extract[A](fa: NonEmptyLazyList[A]): A = fa.head
Expand Down
32 changes: 30 additions & 2 deletions core/src/main/scala-2.13+/cats/instances/lazyList.scala
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
87 changes: 87 additions & 0 deletions core/src/main/scala/cats/Align.scala
@@ -0,0 +1,87 @@
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 => Function.tupled(f)(ior.pad))
LukaJCB marked this conversation as resolved.
Show resolved Hide resolved
LukaJCB marked this conversation as resolved.
Show resolved Hide resolved
}

object Align {
def semigroup[F[_]: Align, A: Semigroup]: Semigroup[F[A]] = new Semigroup[F[A]] {
LukaJCB marked this conversation as resolved.
Show resolved Hide resolved
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
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
@@ -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]
}
}
14 changes: 12 additions & 2 deletions core/src/main/scala/cats/data/AbstractNonEmptyInstances.scala
Expand Up @@ -4,14 +4,17 @@ package data
abstract private[data] class AbstractNonEmptyInstances[F[_], NonEmptyF[_]](implicit MF: Monad[F],
CF: CoflatMap[F],
TF: Traverse[F],
SF: SemigroupK[F])
SF: SemigroupK[F],
AF: Align[F])
extends Bimonad[NonEmptyF]
with NonEmptyTraverse[NonEmptyF]
with SemigroupK[NonEmptyF] {
with SemigroupK[NonEmptyF]
with Align[NonEmptyF] {
val monadInstance = MF.asInstanceOf[Monad[NonEmptyF]]
val coflatMapInstance = CF.asInstanceOf[CoflatMap[NonEmptyF]]
val traverseInstance = Traverse[F].asInstanceOf[Traverse[NonEmptyF]]
val semiGroupKInstance = SemigroupK[F].asInstanceOf[SemigroupK[NonEmptyF]]
val alignInstance = Align[F].asInstanceOf[Align[NonEmptyF]]

def combineK[A](a: NonEmptyF[A], b: NonEmptyF[A]): NonEmptyF[A] =
semiGroupKInstance.combineK(a, b)
Expand Down Expand Up @@ -78,4 +81,11 @@ abstract private[data] class AbstractNonEmptyInstances[F[_], NonEmptyF[_]](impli
override def collectFirstSome[A, B](fa: NonEmptyF[A])(f: A => Option[B]): Option[B] =
traverseInstance.collectFirstSome(fa)(f)

def align[A, B](fa: NonEmptyF[A], fb: NonEmptyF[B]): NonEmptyF[Ior[A, B]] =
alignInstance.align(fa, fb)

override def functor: Functor[NonEmptyF] = alignInstance.functor

override def alignWith[A, B, C](fa: NonEmptyF[A], fb: NonEmptyF[B])(f: Ior[A, B] => C): NonEmptyF[C] =
alignInstance.alignWith(fa, fb)(f)
}
25 changes: 23 additions & 2 deletions core/src/main/scala/cats/data/Chain.scala
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
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
6 changes: 4 additions & 2 deletions core/src/main/scala/cats/data/NonEmptyChain.scala
Expand Up @@ -418,8 +418,10 @@ 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] =
implicit val catsDataInstancesForNonEmptyChain: SemigroupK[NonEmptyChain]
with NonEmptyTraverse[NonEmptyChain]
with Bimonad[NonEmptyChain]
with Align[NonEmptyChain] =
new AbstractNonEmptyInstances[Chain, NonEmptyChain] {
def extract[A](fa: NonEmptyChain[A]): A = fa.head

Expand Down
24 changes: 22 additions & 2 deletions core/src/main/scala/cats/data/NonEmptyList.scala
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
9 changes: 7 additions & 2 deletions core/src/main/scala/cats/data/NonEmptyMapImpl.scala
Expand Up @@ -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)
Expand Down Expand Up @@ -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]] =
Expand Down