Skip to content

Commit

Permalink
backporting typelevel#3076 Added Aligh typeclass
Browse files Browse the repository at this point in the history
  • Loading branch information
gagandeepkalra committed Mar 8, 2020
1 parent cd10e01 commit 5999702
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 0 deletions.
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]

/**
* 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)
}
}
4 changes: 4 additions & 0 deletions core/src/main/scala/cats/syntax/align.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package cats
package syntax

trait AlignSyntax extends Align.ToAlignOps
47 changes: 47 additions & 0 deletions laws/src/main/scala/cats/laws/AlignLaws.scala
Original file line number Diff line number Diff line change
@@ -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 }
}
46 changes: 46 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/AlignTests.scala
Original file line number Diff line number Diff line change
@@ -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] }
}
14 changes: 14 additions & 0 deletions tests/src/test/scala/cats/tests/AlignSuite.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 5999702

Please sign in to comment.