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

Created Copair typeclass #976

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions core/src/main/scala/cats/Copair.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cats

trait Copair[F[_,_]] extends Bitraverse[F] {
def left[A,B](a: A): F[A,B]
def right[A,B](b: B): F[A,B]

def fold[A,B,C](f: F[A,B])(fa: A => C, fb: B => C): C

def isRight[A,B](f: F[A,B]): Boolean = fold(f)(_ => false, _ => true )
def isLeft[A,B](f: F[A,B]): Boolean = fold(f)(_ => true , _ => false)

def foreach[A,B](f: F[A,B])(fn: B => Unit): Unit = fold(f)(_ => (), fn)

def swap[A,B](f: F[A,B]): F[B,A] = fold(f)(right[B, A], left[B, A])

def forall[A, B](f: F[A,B])(fn: B => Boolean): Boolean = fold(f)(_ => true, fn)

def exists[A, B](f: F[A,B])(fn: B => Boolean): Boolean = fold(f)(_ => false, fn)

def to[G[_, _], A, B](f: F[A, B])(implicit G: Copair[G]): G[A,B] = fold(f)(G.left, G.right)

def bitraverse[G[_], A, B, C, D](fab: F[A, B])(f: A => G[C], g: B => G[D])(implicit G: Applicative[G]): G[F[C, D]] =
fold(fab)(
l => G.map(f(l))(left),
r => G.map(g(r))(right)
)

def bifoldLeft[A, B, C](fab: F[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C =
fold(fab)(
l => f(c,l),
r => g(c,r)
)

def bifoldRight[A, B, C](fab: F[A, B], c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C] =
fold(fab)(
l => f(l,c),
r => g(r,c)
)
}
31 changes: 8 additions & 23 deletions core/src/main/scala/cats/data/Validated.scala
Original file line number Diff line number Diff line change
Expand Up @@ -237,31 +237,16 @@ private[data] sealed abstract class ValidatedInstances extends ValidatedInstance
def show(f: Validated[A,B]): String = f.show
}

implicit val validatedBitraverse: Bitraverse[Validated] =
new Bitraverse[Validated] {
def bitraverse[G[_], A, B, C, D](fab: Validated[A, B])(f: A => G[C], g: B => G[D])(implicit G: Applicative[G]): G[Validated[C, D]] =
fab match {
case Invalid(a) => G.map(f(a))(Validated.invalid)
case Valid(b) => G.map(g(b))(Validated.valid)
implicit def validatedCopair: Copair[Validated] =
new Copair[Validated] {
def fold[A, B, C](f: Validated[A, B])(fa: (A) => C, fb: (B) => C): C =
f match {
case Invalid(a) => fa(a)
case Valid(b) => fb(b)
}

def bifoldLeft[A, B, C](fab: Validated[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C =
fab match {
case Invalid(a) => f(c, a)
case Valid(b) => g(c, b)
}

def bifoldRight[A, B, C](fab: Validated[A, B], c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C] =
fab match {
case Invalid(a) => f(a, c)
case Valid(b) => g(b, c)
}

override def bimap[A, B, C, D](fab: Validated[A, B])(f: A => C, g: B => D): Validated[C, D] =
fab.bimap(f, g)

override def leftMap[A, B, C](fab: Validated[A, B])(f: A => C): Validated[C, B] =
fab.leftMap(f)
def left[A, B](a: A): Validated[A, B] = Invalid(a)
def right[A, B](b: B): Validated[A, B] = Valid(b)
}

implicit def validatedInstances[E](implicit E: Semigroup[E]): Traverse[Validated[E, ?]] with ApplicativeError[Validated[E, ?], E] =
Expand Down
24 changes: 8 additions & 16 deletions core/src/main/scala/cats/data/Xor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -176,25 +176,17 @@ private[data] sealed abstract class XorInstances extends XorInstances1 {
}
}

implicit val xorBitraverse: Bitraverse[Xor] =
new Bitraverse[Xor] {
def bitraverse[G[_], A, B, C, D](fab: Xor[A, B])(f: A => G[C], g: B => G[D])(implicit G: Applicative[G]): G[Xor[C, D]] =
fab match {
case Xor.Left(a) => G.map(f(a))(Xor.left)
case Xor.Right(b) => G.map(g(b))(Xor.right)
}

def bifoldLeft[A, B, C](fab: Xor[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C =
fab match {
case Xor.Left(a) => f(c, a)
case Xor.Right(b) => g(c, b)
implicit def xorCopair: Copair[Xor] =
new Copair[Xor] {
def fold[A, B, C](f: Xor[A, B])(fa: (A) => C, fb: (B) => C): C =
f match {
case Xor.Left(a) => fa(a)
case Xor.Right(b) => fb(b)
}

def bifoldRight[A, B, C](fab: Xor[A, B], c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C] =
fab match {
case Xor.Left(a) => f(a, c)
case Xor.Right(b) => g(b, c)
}
def left[A, B](a: A): Xor[A, B] = Xor.Left(a)
def right[A, B](b: B): Xor[A, B] = Xor.Right(b)
}

implicit def xorInstances[A]: Traverse[A Xor ?] with MonadError[Xor[A, ?], A] =
Expand Down
26 changes: 10 additions & 16 deletions core/src/main/scala/cats/std/either.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,20 @@ package cats
package std

trait EitherInstances extends EitherInstances1 {
implicit val eitherBitraverse: Bitraverse[Either] =
new Bitraverse[Either] {
def bitraverse[G[_], A, B, C, D](fab: Either[A, B])(f: A => G[C], g: B => G[D])(implicit G: Applicative[G]): G[Either[C, D]] =
fab match {
case Left(a) => G.map(f(a))(Left(_))
case Right(b) => G.map(g(b))(Right(_))
}

def bifoldLeft[A, B, C](fab: Either[A, B], c: C)(f: (C, A) => C, g: (C, B) => C): C =
fab match {
case Left(a) => f(c, a)
case Right(b) => g(c, b)
implicit val eitherBitraverse: Copair[Either] =
new Copair[Either] {
def fold[A, B, C](f: Either[A, B])(fa: (A) => C, fb: (B) => C): C =
f match {
case Left(a) => fa(a)
case Right(b) => fb(b)
}

def bifoldRight[A, B, C](fab: Either[A, B], c: Eval[C])(f: (A, Eval[C]) => Eval[C], g: (B, Eval[C]) => Eval[C]): Eval[C] =
fab match {
case Left(a) => f(a, c)
case Right(b) => g(b, c)
}
def left[A, B](a: A): Either[A, B] = Left(a)
def right[A, B](b: B): Either[A, B] = Right(b)
}


implicit def eitherInstances[A]: Monad[Either[A, ?]] with Traverse[Either[A, ?]] =
new Monad[Either[A, ?]] with Traverse[Either[A, ?]] {
def pure[B](b: B): Either[A, B] = Right(b)
Expand Down Expand Up @@ -63,6 +56,7 @@ trait EitherInstances extends EitherInstances1 {
}

private[std] sealed trait EitherInstances1 extends EitherInstances2 {

implicit def eitherPartialOrder[A, B](implicit A: PartialOrder[A], B: PartialOrder[B]): PartialOrder[Either[A, B]] =
new PartialOrder[Either[A, B]] {
def partialCompare(x: Either[A, B], y: Either[A, B]): Double = x.fold(
Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/cats/syntax/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ trait AllSyntax
with ValidatedSyntax
with CoproductSyntax
with WriterSyntax
with CopairSyntax
32 changes: 32 additions & 0 deletions core/src/main/scala/cats/syntax/copair.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package cats
package syntax

trait CopairSyntax {
implicit def copairSyntax[F[_, _]: Copair, A, B](fab: F[A, B]): CopairOps[F, A, B] =
new CopairOps[F, A, B](fab)

implicit def copairIdSyntax[A](a: A): CopairIdOps[A] = new CopairIdOps[A](a)

}

final class CopairOps[F[_,_], A, B](pair: F[A,B])(implicit F: Copair[F]) {

def fold[C](fa: A => C, fb: B => C): C = F.fold(pair)(fa, fb)
def swap: F[B,A] = F.swap(pair)

def isRight: Boolean = F.isRight(pair)
def isLeft: Boolean = F.isLeft(pair)

def foreach(fn: B => Unit): Unit = F.foreach(pair)(fn)

def forall(fn: B => Boolean): Boolean = F.forall(pair)(fn)

def exists(fn: B => Boolean): Boolean = F.exists(pair)(fn)

def to[G[_, _]](implicit G: Copair[G]): G[A,B] = F.to[G,A,B](pair)
}

final class CopairIdOps[A](a: A) {
def leftC[F[_,_], B](implicit F: Copair[F]): F[A,B] = F.left(a)
def rightC[F[_,_], B](implicit F: Copair[F]): F[B,A] = F.right(a)
}
1 change: 1 addition & 0 deletions core/src/main/scala/cats/syntax/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ package object syntax {
object xor extends XorSyntax
object validated extends ValidatedSyntax
object writer extends WriterSyntax
object copair extends CopairSyntax
}
35 changes: 35 additions & 0 deletions laws/src/main/scala/cats/laws/CopairLaws.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package cats.laws

import cats.Copair
import cats.syntax.copair._

trait CopairLaws[F[_,_]] extends BitraverseLaws[F] with BifoldableLaws[F] with BifunctorLaws[F] {
implicit override def F: Copair[F]

def copairFoldIdentity[A, B](fab: F[A, B]): IsEq[F[A, B]] =
fab <-> fab.fold(_.leftC[F, B], _.rightC[F, A])

def copairDoubleSwapIdentity[A,B](fab: F[A,B]): IsEq[F[A,B]] =
fab <-> fab.swap.swap

def copairLeftIdentity[A, B, C](a: A, fa: A => C, fb: B => C): IsEq[C] =
a.leftC[F, B].fold(fa, fb) <-> fa(a)

def copairRightIdentity[A, B, C](b: B, fa: A => C, fb: B => C): IsEq[C] =
b.rightC[F, A].fold(fa, fb) <-> fb(b)

def copairToIdentity[A, B](fab: F[A,B]): IsEq[F[A,B]] =
fab.to[F] <-> fab

def copairLeftSwapIdentity[A, B](b: B): IsEq[F[A, B]] =
b.leftC[F, A].swap <-> b.rightC[F, A]

def copairRightSwapIdentity[A, B](a: A): IsEq[F[A, B]] =
a.rightC[F, B].swap <-> a.leftC[F, B]

}

object CopairLaws {
def apply[F[_, _]](implicit ev: Copair[F]): CopairLaws[F] =
new CopairLaws[F] { def F: Copair[F] = ev }
}
52 changes: 52 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/CopairTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package cats.laws.discipline

import cats.{Eq, Monoid, Applicative, Copair}
import cats.laws.CopairLaws
import org.scalacheck.Arbitrary
import org.scalacheck.Prop.forAll

trait CopairTests[F[_,_]] extends BitraverseTests[F] with BifoldableTests[F] with BifunctorTests[F] {
def laws: CopairLaws[F]

def copair[G[_], A, B, C, D, E, H](implicit
G: Applicative[G],
C: Monoid[C],
ArbFAB: Arbitrary[F[A, B]],
ArbFAD: Arbitrary[F[A, D]],
ArbGC: Arbitrary[G[C]],
ArbGD: Arbitrary[G[D]],
ArbGE: Arbitrary[G[E]],
ArbGH: Arbitrary[G[H]],
ArbA: Arbitrary[A],
ArbB: Arbitrary[B],
ArbC: Arbitrary[C],
ArbE: Arbitrary[E],
ArbH: Arbitrary[H],
EqFAB: Eq[F[A, B]],
EqFAD: Eq[F[A, D]],
EqFAH: Eq[F[A, H]],
EqFCD: Eq[F[C, D]],
EqFCH: Eq[F[C, H]],
EqGGFEH: Eq[G[G[F[E, H]]]],
EqC: Eq[C]
): RuleSet =
new RuleSet {
val name = "copair"
val parents = Seq(bitraverse[G,A,B,C,D,E,H], bifoldable[A, B, C], bifunctor[A, B, C, D, E, H])
val bases = Seq.empty
val props = Seq(
"copair fold identity" -> forAll(laws.copairFoldIdentity[A, B] _),
"copair double swap identity" -> forAll(laws.copairDoubleSwapIdentity[A,B] _),
"copair left swap identity" -> forAll(laws.copairLeftSwapIdentity[A, B] _),
"copair right swap identity" -> forAll(laws.copairRightSwapIdentity[A, B] _),
"copair left identity" -> forAll(laws.copairLeftIdentity[A,B,C] _),
"copair right identity" -> forAll(laws.copairRightIdentity[A,B,C] _),
"copair to identity" -> forAll(laws.copairToIdentity[A,B] _)
)
}
}

object CopairTests {
def apply[F[_, _]: Copair]: CopairTests[F] =
new CopairTests[F] { def laws: CopairLaws[F] = CopairLaws[F] }
}
59 changes: 59 additions & 0 deletions tests/src/test/scala/cats/tests/CopairTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package cats.tests

import cats.Copair
import cats.data.{Validated, Xor}
import cats.laws.discipline.CopairTests
import cats.laws.discipline.arbitrary._
import org.scalacheck.Arbitrary

class CopairTest extends CatsSuite {

checkAll("Xor", CopairTests[Xor].copair[Option, Int, Int, Int, String, String, String])

testCopairs[Xor]("Xor")
testCopairs[Either]("Either")
testCopairs[Validated]("Validated")

def testCopairs[F[_,_]: Copair](ofType: String)(implicit arb: Arbitrary[F[String, Int]]): Unit = {

test(s"$ofType Copair for-each performs side-effect") {

forAll { copair: F[String, Int] =>
var sideEffectOccurred = false
copair.foreach(_ => sideEffectOccurred = true)

sideEffectOccurred should === (copair.isRight)
}
}

test(s"$ofType Copair for-all") {
forAll { copair: F[String, Int] =>
copair.forall(_ % 2 == 0) should === (copair.fold(_ => true, _ % 2 == 0))
}
}

test(s"$ofType Copair exists") {
forAll { copair: F[String, Int] =>
copair.exists(_ % 2 == 0) should === (copair.fold(_ => false, _ % 2 == 0))
}
}

test(s"$ofType Copair left/right") {
forAll { copair: F[String, Int] =>
copair.isLeft should === (copair.fold(_ => true, _ => false))
copair.isRight should === (copair.fold(_ => false, _ => true))
}
}

test(s"$ofType Copair to") {
forAll { copair: F[String, Int] =>
copair.to[Xor].isLeft should === (copair.isLeft)
copair.to[Xor].isRight should === (copair.isRight)

val (strFold, intFold): (String => String, Int => String) = (_ => "string", _ => "int")
copair.to[Xor].fold(strFold, intFold) should === (copair.fold(strFold, intFold))
}
}

}
}
4 changes: 2 additions & 2 deletions tests/src/test/scala/cats/tests/EitherTests.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package cats
package tests

import cats.laws.discipline.{BitraverseTests, TraverseTests, MonadTests, SerializableTests, CartesianTests}
import cats.laws.discipline.{CopairTests, TraverseTests, MonadTests, SerializableTests, CartesianTests}
import cats.kernel.laws.OrderLaws

class EitherTests extends CatsSuite {
Expand All @@ -17,7 +17,7 @@ class EitherTests extends CatsSuite {
checkAll("Either[Int, Int] with Option", TraverseTests[Either[Int, ?]].traverse[Int, Int, Int, Int, Option, Option])
checkAll("Traverse[Either[Int, ?]", SerializableTests.serializable(Traverse[Either[Int, ?]]))

checkAll("Either[?, ?]", BitraverseTests[Either].bitraverse[Option, Int, Int, Int, String, String, String])
checkAll("Either[?, ?]", CopairTests[Either].copair[Option, Int, Int, Int, String, String, String])
checkAll("Bitraverse[Either]", SerializableTests.serializable(Bitraverse[Either]))

val partialOrder = eitherPartialOrder[Int, String]
Expand Down
4 changes: 2 additions & 2 deletions tests/src/test/scala/cats/tests/ValidatedTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package tests

import cats.data.{NonEmptyList, Validated, ValidatedNel, Xor, XorT}
import cats.data.Validated.{Valid, Invalid}
import cats.laws.discipline.{BitraverseTests, TraverseTests, ApplicativeErrorTests, SerializableTests, CartesianTests}
import cats.laws.discipline.{CopairTests, TraverseTests, ApplicativeErrorTests, SerializableTests, CartesianTests}
import org.scalacheck.Arbitrary._
import cats.laws.discipline.{SemigroupKTests}
import cats.laws.discipline.arbitrary._
Expand All @@ -16,7 +16,7 @@ class ValidatedTests extends CatsSuite {
checkAll("Validated[String, Int]", CartesianTests[Validated[String,?]].cartesian[Int, Int, Int])
checkAll("Cartesian[Validated[String,?]]", SerializableTests.serializable(Cartesian[Validated[String,?]]))

checkAll("Validated[?, ?]", BitraverseTests[Validated].bitraverse[Option, Int, Int, Int, String, String, String])
checkAll("Validated[?, ?]", CopairTests[Validated].copair[Option, Int, Int, Int, String, String, String])
checkAll("Bitraverse[Validated]", SerializableTests.serializable(Bitraverse[Validated]))

implicit val eq0 = XorT.xorTEq[Validated[String, ?], String, Int]
Expand Down
Loading