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 ContravariantCartesian typeclass and for Tuple2 add Monad and FlatMap #1299

Merged
merged 10 commits into from
Aug 21, 2016
1 change: 1 addition & 0 deletions core/src/main/scala/cats/Cartesian.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ object Cartesian extends CartesianArityFunctions with KernelCartesianInstances
private[cats] sealed trait KernelCartesianInstances {
implicit val catsInvariantSemigroup: Cartesian[Semigroup] = InvariantMonoidal.catsInvariantMonoidalSemigroup
implicit val catsInvariantMonoid: Cartesian[Monoid] = InvariantMonoidal.catsInvariantMonoidalMonoid
implicit val catsCartesianEq: Cartesian[Eq] = ContravariantCartesian.catsContravariantCartesianEq
}
10 changes: 10 additions & 0 deletions core/src/main/scala/cats/Composed.scala
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ private[cats] trait ComposedContravariantCovariant[F[_], G[_]] extends Contravar
F.contramap(fga)(gb => G.map(gb)(f))
}

private[cats] trait ComposedCartesian[F[_], G[_]] extends ContravariantCartesian[λ[α => F[G[α]]]] with ComposedContravariantCovariant[F, G] { outer =>
def F: ContravariantCartesian[F]
def G: Functor[G]

def product[A, B](fa: F[G[A]], fb: F[G[B]]): F[G[(A, B)]] =
F.contramap(F.product(fa, fb)) { g: G[(A, B)] =>
(G.map(g)(_._1), G.map(g)(_._2))
}
}

private[cats] trait ComposedCovariantContravariant[F[_], G[_]] extends Contravariant[λ[α => F[G[α]]]] { outer =>
def F: Functor[F]
def G: Contravariant[G]
Expand Down
33 changes: 33 additions & 0 deletions core/src/main/scala/cats/ContravariantCartesian.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package cats

import functor.Contravariant
import simulacrum.typeclass

/**
* [[ContravariantCartesian]] is nothing more than something both contravariant
* and Cartesian. It comes up enough to be useful, and composes well
*/
@typeclass trait ContravariantCartesian[F[_]] extends Cartesian[F] with Contravariant[F] { self =>
override def composeFunctor[G[_]: Functor]: ContravariantCartesian[λ[α => F[G[α]]]] =
new ComposedCartesian[F, G] {
def F = self
def G = Functor[G]
}
}

object ContravariantCartesian extends KernelContravariantCartesianInstances

private[cats] sealed trait KernelContravariantCartesianInstances extends ContravariantCartesian1 {
implicit val catsContravariantCartesianEq: ContravariantCartesian[Eq] = new ContravariantCartesian[Eq] {
def contramap[A, B](fa: Eq[A])(fn: B => A): Eq[B] = fa.on(fn)
def product[A, B](fa: Eq[A], fb: Eq[B]): Eq[(A, B)] =
Eq.instance { (left, right) => fa.eqv(left._1, right._1) && fb.eqv(left._2, right._2) }
}
}

private[cats] sealed trait ContravariantCartesian1 {
implicit def catsContravariantCartesianFromEach[F[_]](implicit contra: Contravariant[F], cart: Cartesian[F]): ContravariantCartesian[F] = new ContravariantCartesian[F] {
def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] = cart.product(fa, fb)
def contramap[A, B](fa: F[A])(fn: B => A): F[B] = contra.contramap(fa)(fn)
}
}
7 changes: 7 additions & 0 deletions core/src/main/scala/cats/InvariantMonoidal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ private[cats] trait KernelInvariantMonoidalInstances {

def imap[A, B](fa: Semigroup[A])(f: A => B)(g: B => A): Semigroup[B] = new Semigroup[B] {
def combine(x: B, y: B): B = f(fa.combine(g(x), g(y)))
override def combineAllOption(bs: TraversableOnce[B]): Option[B] =
fa.combineAllOption(bs.map(g)).map(f)
}

def pure[A](a: A): Semigroup[A] = new Semigroup[A] {
def combine(x: A, y: A): A = a
override def combineAllOption(as: TraversableOnce[A]): Option[A] =
if (as.isEmpty) None else Some(a)
}
}

Expand All @@ -42,11 +46,14 @@ private[cats] trait KernelInvariantMonoidalInstances {
def imap[A, B](fa: Monoid[A])(f: A => B)(g: B => A): Monoid[B] = new Monoid[B] {
val empty = f(fa.empty)
def combine(x: B, y: B): B = f(fa.combine(g(x), g(y)))
override def combineAll(bs: TraversableOnce[B]): B =
f(fa.combineAll(bs.map(g)))
}

def pure[A](a: A): Monoid[A] = new Monoid[A] {
val empty = a
def combine(x: A, y: A): A = a
override def combineAll(as: TraversableOnce[A]): A = a
}
}
}
10 changes: 2 additions & 8 deletions core/src/main/scala/cats/functor/Contravariant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,8 @@ object Contravariant extends KernelContravariantInstances
* can't have instances for this type class in their companion objects.
*/
private[functor] sealed trait KernelContravariantInstances {
implicit val catsFunctorContravariantForEq: Contravariant[Eq] =
new Contravariant[Eq] {
/** Derive a `Eq` for `B` given a `Eq[A]` and a function `B => A`.
*
* Note: resulting instances are law-abiding only when the functions used are injective (represent a one-to-one mapping)
*/
def contramap[A, B](fa: Eq[A])(f: B => A): Eq[B] = fa.on(f)
}
implicit def catsFunctorContravariantForEq: Contravariant[Eq] =
ContravariantCartesian.catsContravariantCartesianEq

implicit val catsFunctorContravariantForPartialOrder: Contravariant[PartialOrder] =
new Contravariant[PartialOrder] {
Expand Down
61 changes: 60 additions & 1 deletion core/src/main/scala/cats/instances/tuple.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package cats
package instances

import data.Xor
Copy link
Contributor

Choose a reason for hiding this comment

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

Either! :D

import scala.annotation.tailrec

trait TupleInstances extends Tuple2Instances with cats.kernel.instances.TupleInstances

sealed trait Tuple2Instances {
sealed trait Tuple2Instances extends Tuple2Instances1 {
implicit val catsStdBitraverseForTuple2: Bitraverse[Tuple2] =
new Bitraverse[Tuple2] {
def bitraverse[G[_]: Applicative, A, B, C, D](fab: (A, B))(f: A => G[C], g: B => G[D]): G[(C, D)] =
Expand Down Expand Up @@ -40,3 +43,59 @@ sealed trait Tuple2Instances {
override def coflatten[A](fa: (X, A)): (X, (X, A)) = (fa._1, fa)
}
}

sealed trait Tuple2Instances1 extends Tuple2Instances2 {
implicit def catsStdMonadForTuple2[X](implicit MX: Monoid[X]): Monad[(X, ?)] with RecursiveTailRecM[(X, ?)] =
new FlatMapTuple2[X](MX) with Monad[(X, ?)] {
def pure[A](a: A): (X, A) = (MX.empty, a)
}
}

sealed trait Tuple2Instances2 {
implicit def catsStdFlatMapForTuple2[X](implicit SX: Semigroup[X]): FlatMap[(X, ?)] with RecursiveTailRecM[(X, ?)]=
new FlatMapTuple2[X](SX)
}

private[instances] class FlatMapTuple2[X](s: Semigroup[X]) extends FlatMap[(X, ?)] with RecursiveTailRecM[(X, ?)] {
override def ap[A, B](ff: (X, A => B))(fa: (X, A)): (X, B) = {
val x = s.combine(ff._1, fa._1)
val b = ff._2(fa._2)
(x, b)
}

override def product[A, B](fa: (X, A), fb: (X, B)): (X, (A, B)) = {
val x = s.combine(fa._1, fb._1)
(x, (fa._2, fb._2))
}

override def map[A, B](fa: (X, A))(f: A => B): (X, B) =
(fa._1, f(fa._2))

def flatMap[A, B](fa: (X, A))(f: A => (X, B)): (X, B) = {
val xb = f(fa._2)
val x = s.combine(fa._1, xb._1)
(x, xb._2)
}

override def followedBy[A, B](a: (X, A))(b: (X, B)): (X, B) =
(s.combine(a._1, b._1), b._2)

override def mproduct[A, B](fa: (X, A))(f: A => (X, B)): (X, (A, B)) = {
val xb = f(fa._2)
val x = s.combine(fa._1, xb._1)
(x, (fa._2, xb._2))
}

def tailRecM[A, B](a: A)(f: A => (X, A Xor B)): (X, B) = {
@tailrec
def loop(x: X, aa: A): (X, B) =
f(aa) match {
case (nextX, Xor.Left(nextA)) => loop(s.combine(x, nextX), nextA)
case (nextX, Xor.Right(b)) => (s.combine(x, nextX), b)
}
f(a) match {
case (x, Xor.Right(b)) => (x, b)
case (x, Xor.Left(nextA)) => loop(x, nextA)
}
}
}
6 changes: 6 additions & 0 deletions laws/src/main/scala/cats/laws/FlatMapLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ trait FlatMapLaws[F[_]] extends ApplyLaws[F] {
def flatMapConsistentApply[A, B](fa: F[A], fab: F[A => B]): IsEq[F[B]] =
fab.ap(fa) <-> fab.flatMap(f => fa.map(f))

def followedByConsistency[A, B](fa: F[A], fb: F[B]): IsEq[F[B]] =
F.followedBy(fa)(fb) <-> F.flatMap(fa)(_ => fb)

/**
* The composition of `cats.data.Kleisli` arrows is associative. This is
* analogous to [[flatMapAssociativity]].
Expand All @@ -27,6 +30,9 @@ trait FlatMapLaws[F[_]] extends ApplyLaws[F] {
((kf andThen kg) andThen kh).run(a) <-> (kf andThen (kg andThen kh)).run(a)
}

def mproductConsistency[A, B](fa: F[A], fb: A => F[B]): IsEq[F[(A, B)]] =
F.mproduct(fa)(fb) <-> F.flatMap(fa)(a => F.map(fb(a))((a, _)))

def tailRecMConsistentFlatMap[A](count: Int, a: A, f: A => F[A]): IsEq[F[A]] = {
def bounce(n: Int) = F.tailRecM[(A, Int), A]((a, n)) { case (a0, i) =>
if (i > 0) f(a0).map(a1 => Xor.left((a1, i-1)))
Expand Down
6 changes: 6 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/FlatMapTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ trait FlatMapTests[F[_]] extends ApplyTests[F] {
EqFABC: Eq[F[(A, B, C)]],
iso: Isomorphisms[F]
): RuleSet = {
implicit def functorF: Functor[F] = laws.F
implicit val EqFAB: Eq[F[(A, B)]] =
ContravariantCartesian[Eq].composeFunctor[F].product(EqFA, EqFB)

new DefaultRuleSet(
name = "flatMap",
parent = Some(apply[A, B, C]),
"flatMap associativity" -> forAll(laws.flatMapAssociativity[A, B, C] _),
"flatMap consistent apply" -> forAll(laws.flatMapConsistentApply[A, B] _),
"followedBy consistent flatMap" -> forAll(laws.followedByConsistency[A, B] _),
"mproduct consistent flatMap" -> forAll(laws.mproductConsistency[A, B] _),
"tailRecM consistent flatMap" -> forAll(laws.tailRecMConsistentFlatMap[A] _))
}
}
Expand Down
26 changes: 25 additions & 1 deletion tests/src/test/scala/cats/tests/TupleTests.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package cats
package tests

import cats.laws.discipline.{BitraverseTests, ComonadTests, SerializableTests, TraverseTests}
import data.NonEmptyList

import cats.laws.discipline.{
BitraverseTests, ComonadTests, SerializableTests, TraverseTests, MonadTests, FlatMapTests, CartesianTests
}
import cats.laws.discipline.arbitrary._

class TupleTests extends CatsSuite {

implicit val iso1 = CartesianTests.Isomorphisms.invariant[(NonEmptyList[Int], ?)]
implicit val iso2 = CartesianTests.Isomorphisms.invariant[(String, ?)]

checkAll("Tuple2", BitraverseTests[Tuple2].bitraverse[Option, Int, Int, Int, String, String, String])
checkAll("Bitraverse[Tuple2]", SerializableTests.serializable(Bitraverse[Tuple2]))

Expand All @@ -13,6 +22,21 @@ class TupleTests extends CatsSuite {
checkAll("Tuple2[String, Int]", ComonadTests[(String, ?)].comonad[Int, Int, Int])
checkAll("Comonad[(String, ?)]", SerializableTests.serializable(Comonad[(String, ?)]))

// Note that NonEmptyList has no Monoid, so we can make a FlatMap, but not a Monad
checkAll("FlatMap[(NonEmptyList[Int], ?)]", FlatMapTests[(NonEmptyList[Int], ?)].flatMap[String, Long, String])
checkAll("FlatMap[(String, ?)] serializable", SerializableTests.serializable(FlatMap[(String, ?)]))

checkAll("Monad[(String, ?)]", MonadTests[(String, ?)].monad[Int, Int, String])
checkAll("Monad[(String, ?)] serializable", SerializableTests.serializable(Monad[(String, ?)]))

test("Cartesian composition") {
val cart = ContravariantCartesian[Eq].composeFunctor[(Int, ?)]
val eq = cart.product(Eq[(Int, String)], Eq[(Int, Int)])
forAll { (a: (Int, (String, Int)), b: (Int, (String, Int))) =>
(a == b) should === (eq.eqv(a, b))
}
}

test("eqv") {
val eq = Eq[(Int, Long)]
forAll { t: (Int, Long) => eq.eqv(t, t) should === (true) }
Expand Down