diff --git a/laws/src/main/scala/cats/laws/MonoidKLaws.scala b/laws/src/main/scala/cats/laws/MonoidKLaws.scala new file mode 100644 index 0000000000..495e9d1016 --- /dev/null +++ b/laws/src/main/scala/cats/laws/MonoidKLaws.scala @@ -0,0 +1,20 @@ +package cats +package laws + +/** + * Laws that must be obeyed by any [[cats.MonoidK]]. + */ +trait MonoidKLaws[F[_]] extends SemigroupKLaws[F] { + override implicit def F: MonoidK[F] + + def leftIdentity[A](a: F[A]): IsEq[F[A]] = + F.combine(F.empty, a) <-> a + + def rightIdentity[A](a: F[A]): IsEq[F[A]] = + F.combine(a, F.empty) <-> a +} + +object MonoidKLaws { + def apply[F[_]](implicit ev: MonoidK[F]): MonoidKLaws[F] = + new MonoidKLaws[F] { def F = ev } +} diff --git a/laws/src/main/scala/cats/laws/SemigroupKLaws.scala b/laws/src/main/scala/cats/laws/SemigroupKLaws.scala new file mode 100644 index 0000000000..5cadd329d3 --- /dev/null +++ b/laws/src/main/scala/cats/laws/SemigroupKLaws.scala @@ -0,0 +1,17 @@ +package cats +package laws + +/** + * Laws that must be obeyed by any [[cats.SemigroupK]]. + */ +trait SemigroupKLaws[F[_]] { + implicit def F: SemigroupK[F] + + def associative[A](a: F[A], b: F[A], c: F[A]): IsEq[F[A]] = + F.combine(F.combine(a, b), c) <-> F.combine(a, F.combine(b, c)) +} + +object SemigroupKLaws { + def apply[F[_]](implicit ev: SemigroupK[F]): SemigroupKLaws[F] = + new SemigroupKLaws[F] { def F = ev } +} diff --git a/laws/src/main/scala/cats/laws/discipline/ArbitraryK.scala b/laws/src/main/scala/cats/laws/discipline/ArbitraryK.scala index 7da9843910..ac123aecd7 100644 --- a/laws/src/main/scala/cats/laws/discipline/ArbitraryK.scala +++ b/laws/src/main/scala/cats/laws/discipline/ArbitraryK.scala @@ -55,4 +55,10 @@ object ArbitraryK { def synthesize[A](implicit A: Arbitrary[A]): Arbitrary[Future[A]] = Arbitrary(A.arbitrary.map(Future.successful)) } + + implicit val stream: ArbitraryK[Stream] = + new ArbitraryK[Stream] { def synthesize[A: Arbitrary]: Arbitrary[Stream[A]] = implicitly } + + implicit val vector: ArbitraryK[Vector] = + new ArbitraryK[Vector] { def synthesize[A: Arbitrary]: Arbitrary[Vector[A]] = implicitly } } diff --git a/laws/src/main/scala/cats/laws/discipline/MonoidKTests.scala b/laws/src/main/scala/cats/laws/discipline/MonoidKTests.scala new file mode 100644 index 0000000000..6300549d77 --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/MonoidKTests.scala @@ -0,0 +1,32 @@ +package cats +package laws +package discipline + +import org.scalacheck.Prop._ +import org.scalacheck.Arbitrary + +trait MonoidKTests[F[_]] extends SemigroupKTests[F] { + def laws: MonoidKLaws[F] + + def monoidK[A: Arbitrary](implicit + ArbF: ArbitraryK[F], + EqFA: Eq[F[A]] + ): RuleSet = { + implicit def ArbFA: Arbitrary[F[A]] = ArbF.synthesize[A] + + new RuleSet { + val name = "monoidK" + val bases = Nil + val parents = Seq(semigroupK[A]) + val props = Seq( + "left identity" -> forAll(laws.leftIdentity[A] _), + "right identity" -> forAll(laws.rightIdentity[A] _) + ) + } + } +} + +object MonoidKTests { + def apply[F[_] : MonoidK]: MonoidKTests[F] = + new MonoidKTests[F] { def laws = MonoidKLaws[F] } +} diff --git a/laws/src/main/scala/cats/laws/discipline/SemigroupKTests.scala b/laws/src/main/scala/cats/laws/discipline/SemigroupKTests.scala new file mode 100644 index 0000000000..0e14007e46 --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/SemigroupKTests.scala @@ -0,0 +1,32 @@ +package cats +package laws +package discipline + +import org.scalacheck.Prop._ +import org.scalacheck.Arbitrary +import org.typelevel.discipline.Laws + +trait SemigroupKTests[F[_]] extends Laws { + def laws: SemigroupKLaws[F] + + def semigroupK[A: Arbitrary](implicit + ArbF: ArbitraryK[F], + EqFA: Eq[F[A]] + ): RuleSet = { + implicit def ArbFA: Arbitrary[F[A]] = ArbF.synthesize[A] + + new RuleSet { + val name = "semigroupK" + val bases = Nil + val parents = Nil + val props = Seq( + "associative" -> forAll(laws.associative[A] _) + ) + } + } +} + +object SemigroupKTests { + def apply[F[_]: SemigroupK]: SemigroupKTests[F] = + new SemigroupKTests[F] { def laws = SemigroupKLaws[F] } +} diff --git a/std/src/main/scala/cats/std/stream.scala b/std/src/main/scala/cats/std/stream.scala index 89eee4e1fa..cf1f80cdf1 100644 --- a/std/src/main/scala/cats/std/stream.scala +++ b/std/src/main/scala/cats/std/stream.scala @@ -1,7 +1,7 @@ package cats package std -import scala.annotation.tailrec +import scala.collection.immutable.Stream.Empty trait StreamInstances { implicit val streamInstance: Traverse[Stream] with MonadCombine[Stream] with CoflatMap[Stream] = @@ -50,4 +50,24 @@ trait StreamInstances { G.map(gsb)(_.value) } } + + // TODO: eventually use algebra's instances (which will deal with + // implicit priority between Eq/PartialOrder/Order). + + implicit def eqStream[A](implicit ev: Eq[A]): Eq[Stream[A]] = + new Eq[Stream[A]] { + def eqv(x: Stream[A], y: Stream[A]): Boolean = { + def loop(xs: Stream[A], ys: Stream[A]): Boolean = + xs match { + case Empty => ys.isEmpty + case a #:: xs => + ys match { + case Empty => false + case b #:: ys => if (ev.neqv(a, b)) false else loop(xs, ys) + } + } + loop(x, y) + } + } + } diff --git a/std/src/main/scala/cats/std/vector.scala b/std/src/main/scala/cats/std/vector.scala index 097e810758..557d049897 100644 --- a/std/src/main/scala/cats/std/vector.scala +++ b/std/src/main/scala/cats/std/vector.scala @@ -1,7 +1,6 @@ package cats package std -import scala.annotation.tailrec import scala.collection.immutable.VectorBuilder trait VectorInstances { @@ -39,4 +38,23 @@ trait VectorInstances { G.map(gbb)(_.result) } } + + // TODO: eventually use algebra's instances (which will deal with + // implicit priority between Eq/PartialOrder/Order). + + implicit def eqVector[A](implicit ev: Eq[A]): Eq[Vector[A]] = + new Eq[Vector[A]] { + def eqv(x: Vector[A], y: Vector[A]): Boolean = { + def loop(xs: Vector[A], ys: Vector[A]): Boolean = + xs match { + case Seq() => ys.isEmpty + case a +: xs => + ys match { + case Seq() => false + case b +: ys => if (ev.neqv(a, b)) false else loop(xs, ys) + } + } + loop(x, y) + } + } } diff --git a/tests/src/test/scala/cats/tests/StdTests.scala b/tests/src/test/scala/cats/tests/StdTests.scala index 697fae8e6c..3a22581e8e 100644 --- a/tests/src/test/scala/cats/tests/StdTests.scala +++ b/tests/src/test/scala/cats/tests/StdTests.scala @@ -1,11 +1,15 @@ package cats.tests -import cats.laws.discipline.{ComonadTests, MonadTests, MonadFilterTests} +import cats.laws.discipline.{ComonadTests, MonadTests, MonadFilterTests, MonoidKTests} class StdTests extends CatsSuite { checkAll("Function0[Int]", ComonadTests[Function0].comonad[Int, Int, Int]) checkAll("Function0[Int]", MonadTests[Function0].monad[Int, Int, Int]) checkAll("Option[Int]", MonadFilterTests[Option].monadFilter[Int, Int, Int]) checkAll("Option[String]", MonadFilterTests[Option].monadFilter[String, Int, Int]) + checkAll("Option[Int]", MonoidKTests[Option].monoidK[Int]) checkAll("List[Int]", MonadFilterTests[List].monadFilter[Int, Int, Int]) + checkAll("List[Int]", MonoidKTests[List].monoidK[Int]) + checkAll("Stream[Int]", MonoidKTests[Stream].monoidK[Int]) + checkAll("Vector[Int]", MonoidKTests[Vector].monoidK[Int]) }