Skip to content

Commit

Permalink
Add a Bimonad law.
Browse files Browse the repository at this point in the history
The law requires that extract(pure(a)) = a.

This commit also updates the Monad* tests to use the new EqK
typeclass, and also relocates some laws from ComonadLaws to
CoflatMapLaws.

There is a fair amount of plubming that had to change to get
all of this working. A really important but thankless task
will be to go through all of our tests and use EqK and
ArbitraryK where possible to create a more consistent
experience. This will only get harder once we have a new
ScalaCheck release and we have to worry about Cogen as well.
  • Loading branch information
non committed Sep 2, 2015
1 parent 6476e60 commit fa6457a
Show file tree
Hide file tree
Showing 17 changed files with 195 additions and 91 deletions.
6 changes: 4 additions & 2 deletions core/src/main/scala/cats/std/future.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cats
package std

import cats.syntax.eq._

import scala.concurrent.{Await, ExecutionContext, Future}
import scala.concurrent.duration.FiniteDuration

Expand All @@ -26,8 +28,8 @@ trait FutureInstances extends FutureInstances1 {

def futureEq[A](atMost: FiniteDuration)(implicit A: Eq[A], ec: ExecutionContext): Eq[Future[A]] =
new Eq[Future[A]] {

def eqv(x: Future[A], y: Future[A]): Boolean = Await.result((x zip y).map((A.eqv _).tupled), atMost)
def eqv(fx: Future[A], fy: Future[A]): Boolean =
Await.result((fx zip fy).map { case (x, y) => x === y }, atMost)
}
}

Expand Down
17 changes: 17 additions & 0 deletions laws/shared/src/main/scala/cats/laws/BimonadLaws.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package cats
package laws

/**
* Laws that must be obeyed by any [[Bimonad]].
*/
trait BimonadLaws[F[_]] extends MonadLaws[F] with ComonadLaws[F] {
implicit override def F: Bimonad[F]

def pureExtractComposition[A](a: A): IsEq[A] =
F.extract(F.pure(a)) <-> a
}

object BimonadLaws {
def apply[F[_]](implicit ev: Bimonad[F]): BimonadLaws[F] =
new BimonadLaws[F] { def F: Bimonad[F] = ev }
}
11 changes: 10 additions & 1 deletion laws/shared/src/main/scala/cats/laws/CoflatMapLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package cats
package laws

import cats.data.Cokleisli
import cats.syntax.coflatMap._
import cats.implicits._

/**
* Laws that must be obeyed by any `CoflatMap`.
Expand All @@ -13,6 +13,15 @@ trait CoflatMapLaws[F[_]] extends FunctorLaws[F] {
def coflatMapAssociativity[A, B, C](fa: F[A], f: F[A] => B, g: F[B] => C): IsEq[F[C]] =
fa.coflatMap(f).coflatMap(g) <-> fa.coflatMap(x => g(x.coflatMap(f)))

def coflattenThroughMap[A](fa: F[A]): IsEq[F[F[F[A]]]] =
fa.coflatten.coflatten <-> fa.coflatten.map(_.coflatten)

def coflattenCoherence[A, B](fa: F[A], f: F[A] => B): IsEq[F[B]] =
fa.coflatMap(f) <-> fa.coflatten.map(f)

def coflatMapIdentity[A, B](fa: F[A]): IsEq[F[F[A]]] =
fa.coflatten <-> fa.coflatMap(identity)

/**
* The composition of `cats.data.Cokleisli` arrows is associative. This is
* analogous to [[coflatMapAssociativity]].
Expand Down
9 changes: 0 additions & 9 deletions laws/shared/src/main/scala/cats/laws/ComonadLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,6 @@ trait ComonadLaws[F[_]] extends CoflatMapLaws[F] {
def mapCoflattenIdentity[A](fa: F[A]): IsEq[F[A]] =
fa.coflatten.map(_.extract) <-> fa

def coflattenThroughMap[A](fa: F[A]): IsEq[F[F[F[A]]]] =
fa.coflatten.coflatten <-> fa.coflatten.map(_.coflatten)

def coflattenCoherence[A, B](fa: F[A], f: F[A] => B): IsEq[F[B]] =
fa.coflatMap(f) <-> fa.coflatten.map(f)

def coflatMapIdentity[A, B](fa: F[A]): IsEq[F[F[A]]] =
fa.coflatten <-> fa.coflatMap(identity)

def mapCoflatMapCoherence[A, B](fa: F[A], f: A => B): IsEq[F[B]] =
fa.map(f) <-> fa.coflatMap(fa0 => f(fa0.extract))

Expand Down
8 changes: 7 additions & 1 deletion laws/shared/src/main/scala/cats/laws/MonadLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package cats
package laws

import cats.data.Kleisli
import cats.syntax.flatMap._
import cats.implicits._

/**
* Laws that must be obeyed by any `Monad`.
Expand All @@ -29,6 +29,12 @@ trait MonadLaws[F[_]] extends ApplicativeLaws[F] with FlatMapLaws[F] {
*/
def kleisliRightIdentity[A, B](a: A, f: A => F[B]): IsEq[F[B]] =
(Kleisli(f) andThen Kleisli(F.pure[B])).run(a) <-> f(a)

/**
* Make sure that map and flatMap are consistent.
*/
def mapFlatMapCoherence[A, B](fa: F[A], f: A => B): IsEq[F[B]] =
fa.flatMap(a => F.pure(f(a))) <-> fa.map(f)
}

object MonadLaws {
Expand Down
30 changes: 30 additions & 0 deletions laws/shared/src/main/scala/cats/laws/discipline/BimonadTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cats
package laws
package discipline

import org.scalacheck.Arbitrary
import org.scalacheck.Prop
import Prop._

trait BimonadTests[F[_]] extends MonadTests[F] with ComonadTests[F] {
def laws: BimonadLaws[F]

def bimonad[A: Arbitrary: Eq, B: Arbitrary: Eq, C: Arbitrary: Eq]: RuleSet =
new RuleSet {
def name: String = "bimonad"
def bases: Seq[(String, RuleSet)] = Nil
def parents: Seq[RuleSet] = Seq(monad[A, B, C], comonad[A, B, C])
def props: Seq[(String, Prop)] = Seq(
"pure and extract compose" -> forAll(laws.pureExtractComposition[A] _)
)
}
}

object BimonadTests {
def apply[F[_]: Bimonad: ArbitraryK: EqK]: BimonadTests[F] =
new BimonadTests[F] {
def arbitraryK: ArbitraryK[F] = implicitly
def eqK: EqK[F] = implicitly
def laws: BimonadLaws[F] = BimonadLaws[F]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ trait ComonadTests[F[_]] extends CoflatMapTests[F] {
def laws: ComonadLaws[F]

def comonad[A: Arbitrary: Eq, B: Arbitrary: Eq, C: Arbitrary: Eq]: RuleSet = {
implicit def ArbFA: Arbitrary[F[A]] = ArbitraryK[F].synthesize[A]

implicit val eqfa: Eq[F[A]] = EqK[F].synthesize[A]
implicit val eqffa: Eq[F[F[A]]] = EqK[F].synthesize[F[A]]
implicit val eqfffa: Eq[F[F[F[A]]]] = EqK[F].synthesize[F[F[A]]]
implicit val eqfb: Eq[F[B]] = EqK[F].synthesize[B]
implicit val eqfc: Eq[F[C]] = EqK[F].synthesize[C]
implicit def ArbFA: Arbitrary[F[A]] = ArbitraryK[F].synthesize
implicit val eqfa: Eq[F[A]] = EqK[F].synthesize
implicit val eqffa: Eq[F[F[A]]] = EqK[F].synthesize
implicit val eqfffa: Eq[F[F[F[A]]]] = EqK[F].synthesize
implicit val eqfb: Eq[F[B]] = EqK[F].synthesize
implicit val eqfc: Eq[F[C]] = EqK[F].synthesize

new DefaultRuleSet(
name = "comonad",
Expand Down
30 changes: 25 additions & 5 deletions laws/shared/src/main/scala/cats/laws/discipline/EqK.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ package cats
package laws
package discipline

import cats.data.{Cokleisli, Kleisli, NonEmptyList, Validated, Xor, XorT, Ior, Const}
import org.scalacheck.Arbitrary

import cats.data._
import cats.implicits._

import scala.concurrent.Future
import org.scalacheck.Arbitrary

trait EqK[F[_]] {
def synthesize[A: Eq]: Eq[F[A]]
Expand Down Expand Up @@ -85,7 +83,6 @@ object EqK {
implicit val vector: EqK[Vector] =
new EqK[Vector] { def synthesize[A: Eq]: Eq[Vector[A]] = implicitly }

import cats.data.{Streaming, StreamingT}
implicit val streaming: EqK[Streaming] =
new EqK[Streaming] { def synthesize[A: Eq]: Eq[Streaming[A]] = implicitly }

Expand All @@ -96,4 +93,27 @@ object EqK {
implicitly
}
}

implicit def function1L[A: Arbitrary]: EqK[A => ?] =
new EqK[A => ?] {
def synthesize[B: Eq]: Eq[A => B] =
cats.laws.discipline.eq.function1Eq
}

implicit def kleisli[F[_]: EqK, A](implicit evKA: EqK[A => ?]): EqK[Kleisli[F, A, ?]] =
new EqK[Kleisli[F, A, ?]] {
def synthesize[B: Eq]: Eq[Kleisli[F, A, B]] = {
implicit val eqFB: Eq[F[B]] = EqK[F].synthesize[B]
implicit val eqAFB: Eq[A => F[B]] = evKA.synthesize[F[B]]
eqAFB.on[Kleisli[F, A, B]](_.run)
}
}

implicit def optionT[F[_]: EqK]: EqK[OptionT[F, ?]] =
new EqK[OptionT[F, ?]] {
def synthesize[A: Eq]: Eq[OptionT[F, A]] = {
implicit val eqFOA: Eq[F[Option[A]]] = EqK[F].synthesize[Option[A]]
implicitly
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ import Prop._
trait MonadCombineTests[F[_]] extends MonadFilterTests[F] with AlternativeTests[F] {
def laws: MonadCombineLaws[F]

def monadCombine[A: Arbitrary, B: Arbitrary, C: Arbitrary](implicit
ArbF: ArbitraryK[F],
EqFA: Eq[F[A]],
EqFB: Eq[F[B]],
EqFC: Eq[F[C]],
arbFAB: Arbitrary[F[A => B]]
): RuleSet = {
implicit def ArbFA: Arbitrary[F[A]] = ArbF.synthesize[A]
implicit def ArbFB: Arbitrary[F[B]] = ArbF.synthesize[B]
def monadCombine[A: Arbitrary: Eq, B: Arbitrary: Eq, C: Arbitrary: Eq]: RuleSet = {
implicit def ArbFA: Arbitrary[F[A]] = ArbitraryK[F].synthesize
implicit def ArbFB: Arbitrary[F[B]] = ArbitraryK[F].synthesize
implicit def ArbFAB: Arbitrary[F[A => B]] = ArbitraryK[F].synthesize
implicit def EqFA: Eq[F[A]] = EqK[F].synthesize
implicit def EqFB: Eq[F[B]] = EqK[F].synthesize
implicit def EqFC: Eq[F[C]] = EqK[F].synthesize

new RuleSet {
def name: String = "monadCombine"
Expand All @@ -31,6 +29,10 @@ trait MonadCombineTests[F[_]] extends MonadFilterTests[F] with AlternativeTests[
}

object MonadCombineTests {
def apply[F[_]: MonadCombine]: MonadCombineTests[F] =
new MonadCombineTests[F] { def laws: MonadCombineLaws[F] = MonadCombineLaws[F] }
def apply[F[_]: MonadCombine: ArbitraryK: EqK]: MonadCombineTests[F] =
new MonadCombineTests[F] {
def arbitraryK: ArbitraryK[F] = implicitly
def eqK: EqK[F] = implicitly
def laws: MonadCombineLaws[F] = MonadCombineLaws[F]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import org.scalacheck.Prop.forAll
trait MonadErrorTests[F[_, _], E] extends MonadTests[F[E, ?]] {
def laws: MonadErrorLaws[F, E]

def monadError[A : Arbitrary, B : Arbitrary, C : Arbitrary](implicit
ArbF: ArbitraryK[F[E, ?]],
EqFA: Eq[F[E, A]],
EqFB: Eq[F[E, B]],
EqFC: Eq[F[E, C]],
ArbE: Arbitrary[E]
): RuleSet = {
implicit def ArbFEA: Arbitrary[F[E, A]] = ArbF.synthesize[A]
implicit def ArbFEB: Arbitrary[F[E, B]] = ArbF.synthesize[B]
implicit def arbitraryK: ArbitraryK[F[E, ?]]
implicit def eqK: EqK[F[E, ?]]

implicit def arbitraryE: Arbitrary[E]
implicit def eqE: Eq[E]

def monadError[A: Arbitrary: Eq, B: Arbitrary: Eq, C: Arbitrary: Eq]: RuleSet = {
implicit def ArbFEA: Arbitrary[F[E, A]] = arbitraryK.synthesize[A]
implicit def ArbFEB: Arbitrary[F[E, B]] = arbitraryK.synthesize[B]
implicit def EqFEA: Eq[F[E, A]] = eqK.synthesize[A]
implicit def EqFEB: Eq[F[E, B]] = eqK.synthesize[B]

new RuleSet {
def name: String = "monadError"
Expand All @@ -32,6 +34,12 @@ trait MonadErrorTests[F[_, _], E] extends MonadTests[F[E, ?]] {
}

object MonadErrorTests {
def apply[F[_, _], E](implicit FE: MonadError[F, E]): MonadErrorTests[F, E] =
new MonadErrorTests[F, E] { def laws: MonadErrorLaws[F, E] = MonadErrorLaws[F, E] }
def apply[F[_, _], E: Arbitrary: Eq](implicit FE: MonadError[F, E], ArbKFE: ArbitraryK[F[E, ?]], EqKFE: EqK[F[E, ?]]): MonadErrorTests[F, E] =
new MonadErrorTests[F, E] {
def arbitraryE: Arbitrary[E] = implicitly[Arbitrary[E]]
def arbitraryK: ArbitraryK[F[E, ?]] = ArbKFE
def eqE: Eq[E] = Eq[E]
def eqK: EqK[F[E, ?]] = EqKFE
def laws: MonadErrorLaws[F, E] = MonadErrorLaws[F, E]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,10 @@ import Prop._
trait MonadFilterTests[F[_]] extends MonadTests[F] {
def laws: MonadFilterLaws[F]

def monadFilter[A: Arbitrary, B: Arbitrary, C: Arbitrary](implicit
ArbF: ArbitraryK[F],
EqFA: Eq[F[A]],
EqFB: Eq[F[B]],
EqFC: Eq[F[C]]
): RuleSet = {
implicit def ArbFA: Arbitrary[F[A]] = ArbF.synthesize[A]
implicit def ArbFB: Arbitrary[F[B]] = ArbF.synthesize[B]
def monadFilter[A: Arbitrary: Eq, B: Arbitrary: Eq, C: Arbitrary: Eq]: RuleSet = {
implicit def ArbFA: Arbitrary[F[A]] = ArbitraryK[F].synthesize
implicit def ArbFB: Arbitrary[F[B]] = ArbitraryK[F].synthesize
implicit def EqFB: Eq[F[B]] = EqK[F].synthesize

new DefaultRuleSet(
name = "monadFilter",
Expand All @@ -27,6 +23,10 @@ trait MonadFilterTests[F[_]] extends MonadTests[F] {
}

object MonadFilterTests {
def apply[F[_]: MonadFilter]: MonadFilterTests[F] =
new MonadFilterTests[F] { def laws: MonadFilterLaws[F] = MonadFilterLaws[F] }
def apply[F[_]: MonadFilter: ArbitraryK: EqK]: MonadFilterTests[F] =
new MonadFilterTests[F] {
def arbitraryK: ArbitraryK[F] = implicitly
def eqK: EqK[F] = implicitly
def laws: MonadFilterLaws[F] = MonadFilterLaws[F]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import org.scalacheck.Prop.forAll
trait MonadReaderTests[F[_, _], R] extends MonadTests[F[R, ?]] {
def laws: MonadReaderLaws[F, R]

def monadReader[A : Arbitrary, B : Arbitrary, C : Arbitrary](implicit
implicit def arbitraryK: ArbitraryK[F[R, ?]]
implicit def eqK: EqK[F[R, ?]]

def monadReader[A: Arbitrary: Eq, B: Arbitrary: Eq, C: Arbitrary: Eq](implicit
ArbF: ArbitraryK[F[R, ?]],
EqFA: Eq[F[R, A]],
EqFB: Eq[F[R, B]],
Expand All @@ -34,6 +37,10 @@ trait MonadReaderTests[F[_, _], R] extends MonadTests[F[R, ?]] {
}

object MonadReaderTests {
def apply[F[_, _], R](implicit FR: MonadReader[F, R]): MonadReaderTests[F, R] =
new MonadReaderTests[F, R] { def laws: MonadReaderLaws[F, R] = MonadReaderLaws[F, R] }
def apply[F[_, _], R](implicit FR: MonadReader[F, R], arbKFR: ArbitraryK[F[R, ?]], eqKFR: EqK[F[R, ?]]): MonadReaderTests[F, R] =
new MonadReaderTests[F, R] {
def arbitraryK: ArbitraryK[F[R, ?]] = arbKFR
def eqK: EqK[F[R, ?]] = eqKFR
def laws: MonadReaderLaws[F, R] = MonadReaderLaws[F, R]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import org.scalacheck.Prop.forAll
trait MonadStateTests[F[_, _], S] extends MonadTests[F[S, ?]] {
def laws: MonadStateLaws[F, S]

def monadState[A : Arbitrary, B : Arbitrary, C : Arbitrary](implicit
implicit def arbitraryK: ArbitraryK[F[S, ?]]
implicit def eqK: EqK[F[S, ?]]

def monadState[A: Arbitrary: Eq, B: Arbitrary: Eq, C: Arbitrary: Eq](implicit
ArbF: ArbitraryK[F[S, ?]],
EqFA: Eq[F[S, A]],
EqFB: Eq[F[S, B]],
Expand All @@ -35,6 +38,10 @@ trait MonadStateTests[F[_, _], S] extends MonadTests[F[S, ?]] {
}

object MonadStateTests {
def apply[F[_, _], S](implicit FS: MonadState[F, S]): MonadStateTests[F, S] =
new MonadStateTests[F, S] { def laws: MonadStateLaws[F, S] = MonadStateLaws[F, S] }
def apply[F[_, _], S](implicit FS: MonadState[F, S], arbKFS: ArbitraryK[F[S, ?]], eqKFS: EqK[F[S, ?]]): MonadStateTests[F, S] =
new MonadStateTests[F, S] {
def arbitraryK: ArbitraryK[F[S, ?]] = arbKFS
def eqK: EqK[F[S, ?]] = eqKFS
def laws: MonadStateLaws[F, S] = MonadStateLaws[F, S]
}
}
25 changes: 15 additions & 10 deletions laws/shared/src/main/scala/cats/laws/discipline/MonadTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import Prop._
trait MonadTests[F[_]] extends ApplicativeTests[F] with FlatMapTests[F] {
def laws: MonadLaws[F]

def monad[A: Arbitrary, B: Arbitrary, C: Arbitrary](implicit
ArbF: ArbitraryK[F],
EqFA: Eq[F[A]],
EqFB: Eq[F[B]],
EqFC: Eq[F[C]]
): RuleSet = {
implicit def ArbFA: Arbitrary[F[A]] = ArbF.synthesize[A]
implicit def ArbFB: Arbitrary[F[B]] = ArbF.synthesize[B]
implicit def arbitraryK: ArbitraryK[F]
implicit def eqK: EqK[F]

def monad[A: Arbitrary: Eq, B: Arbitrary: Eq, C: Arbitrary: Eq]: RuleSet = {
implicit def ArbFA: Arbitrary[F[A]] = ArbitraryK[F].synthesize
implicit def ArbFB: Arbitrary[F[B]] = ArbitraryK[F].synthesize
implicit val eqfa: Eq[F[A]] = EqK[F].synthesize
implicit val eqfb: Eq[F[B]] = EqK[F].synthesize
implicit val eqfc: Eq[F[C]] = EqK[F].synthesize

new RuleSet {
def name: String = "monad"
Expand All @@ -31,6 +32,10 @@ trait MonadTests[F[_]] extends ApplicativeTests[F] with FlatMapTests[F] {
}

object MonadTests {
def apply[F[_]: Monad]: MonadTests[F] =
new MonadTests[F] { def laws: MonadLaws[F] = MonadLaws[F] }
def apply[F[_]: Monad: ArbitraryK: EqK]: MonadTests[F] =
new MonadTests[F] {
def arbitraryK: ArbitraryK[F] = implicitly
def eqK: EqK[F] = implicitly
def laws: MonadLaws[F] = MonadLaws[F]
}
}
Loading

0 comments on commit fa6457a

Please sign in to comment.