diff --git a/core/src/main/scala/cats/Reducible.scala b/core/src/main/scala/cats/Reducible.scala index 20a38bf1ae..2f92905e38 100644 --- a/core/src/main/scala/cats/Reducible.scala +++ b/core/src/main/scala/cats/Reducible.scala @@ -97,7 +97,7 @@ import simulacrum.typeclass * that we only need `Apply[G]` here, since we don't need to call * `Applicative#pure` for a starting value. */ - def sequence1_[G[_], A, B](fga: F[G[A]])(implicit G: Apply[G]): G[Unit] = + def sequence1_[G[_], A](fga: F[G[A]])(implicit G: Apply[G]): G[Unit] = G.map(reduceLeft(fga)((x, y) => G.map2(x, y)((_, b) => b)))(_ => ()) /** diff --git a/laws/src/main/scala/cats/laws/ReducibleLaws.scala b/laws/src/main/scala/cats/laws/ReducibleLaws.scala index 5075a97ab6..937a23c35d 100644 --- a/laws/src/main/scala/cats/laws/ReducibleLaws.scala +++ b/laws/src/main/scala/cats/laws/ReducibleLaws.scala @@ -21,6 +21,12 @@ trait ReducibleLaws[F[_]] extends FoldableLaws[F] { B: Semigroup[B] ): IsEq[B] = fa.reduceMap(f) <-> fa.reduceRightTo(f)((a, eb) => eb.map(f(a) |+| _)).value + + def traverseConsistent[G[_]: Applicative, A, B](fa: F[A], f: A => G[B]): IsEq[G[Unit]] = + fa.traverse1_(f) <-> fa.traverse_(f) + + def sequenceConsistent[G[_]: Applicative, A](fa: F[G[A]]): IsEq[G[Unit]] = + fa.sequence1_ <-> fa.sequence_ } object ReducibleLaws { diff --git a/laws/src/main/scala/cats/laws/discipline/ReducibleTests.scala b/laws/src/main/scala/cats/laws/discipline/ReducibleTests.scala index cb1f343aa6..4f7db2d858 100644 --- a/laws/src/main/scala/cats/laws/discipline/ReducibleTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/ReducibleTests.scala @@ -8,16 +8,21 @@ import org.scalacheck.Prop.forAll trait ReducibleTests[F[_]] extends FoldableTests[F] { def laws: ReducibleLaws[F] - def reducible[A: Arbitrary, B: Arbitrary](implicit + def reducible[G[_]: Applicative, A: Arbitrary, B: Arbitrary](implicit ArbFA: Arbitrary[F[A]], + ArbFGA: Arbitrary[F[G[A]]], + ArbGB: Arbitrary[G[B]], B: Monoid[B], + EqG: Eq[G[Unit]], EqB: Eq[B] ): RuleSet = new DefaultRuleSet( name = "reducible", parent = Some(foldable[A, B]), "reduceLeftTo consistent with reduceMap" -> forAll(laws.reduceLeftToConsistentWithReduceMap[A, B] _), - "reduceRightTo consistent with reduceMap" -> forAll(laws.reduceRightToConsistentWithReduceMap[A, B] _) + "reduceRightTo consistent with reduceMap" -> forAll(laws.reduceRightToConsistentWithReduceMap[A, B] _), + "traverse1_ consistent with traverse_" -> forAll(laws.traverseConsistent[G, A, B] _), + "sequence1_ consistent with sequence_" -> forAll(laws.sequenceConsistent[G, A] _) ) } diff --git a/tests/src/test/scala/cats/tests/OneAndTests.scala b/tests/src/test/scala/cats/tests/OneAndTests.scala index 29768816ed..ad711ea410 100644 --- a/tests/src/test/scala/cats/tests/OneAndTests.scala +++ b/tests/src/test/scala/cats/tests/OneAndTests.scala @@ -5,16 +5,20 @@ import algebra.laws.{GroupLaws, OrderLaws} import cats.data.{NonEmptyList, OneAnd} import cats.laws.discipline.{ComonadTests, FunctorTests, SemigroupKTests, FoldableTests, MonadTests, SerializableTests, CartesianTests, TraverseTests, ReducibleTests} -import cats.laws.discipline.arbitrary.oneAndArbitrary +import cats.laws.discipline.arbitrary._ import cats.laws.discipline.eq._ class OneAndTests extends CatsSuite { + // Lots of collections here.. telling ScalaCheck to calm down a bit + implicit override val generatorDrivenConfig: PropertyCheckConfiguration = + PropertyCheckConfig(maxSize = 5, minSuccessful = 20) + checkAll("OneAnd[List, Int]", OrderLaws[OneAnd[List, Int]].eqv) checkAll("OneAnd[List, Int] with Option", TraverseTests[OneAnd[List, ?]].traverse[Int, Int, Int, Int, Option, Option]) checkAll("Traverse[OneAnd[List, A]]", SerializableTests.serializable(Traverse[OneAnd[List, ?]])) - checkAll("OneAnd[List, Int]", ReducibleTests[OneAnd[List, ?]].reducible[Int, Int]) + checkAll("OneAnd[List, Int]", ReducibleTests[OneAnd[List, ?]].reducible[Option, Int, Int]) checkAll("Reducible[OneAnd[List, ?]]", SerializableTests.serializable(Reducible[OneAnd[List, ?]])) implicit val iso = CartesianTests.Isomorphisms.invariant[OneAnd[ListWrapper, ?]](OneAnd.oneAndFunctor(ListWrapper.functor)) @@ -117,4 +121,46 @@ class OneAndTests extends CatsSuite { nel.map(p).unwrap should === (list.map(p)) } } + + test("reduceLeft consistent with foldLeft") { + forAll { (nel: NonEmptyList[Int], f: (Int, Int) => Int) => + nel.reduceLeft(f) should === (nel.tail.foldLeft(nel.head)(f)) + } + } + + test("reduceRight consistent with foldRight") { + forAll { (nel: NonEmptyList[Int], f: (Int, Eval[Int]) => Eval[Int]) => + nel.reduceRight(f).value should === (nel.tail.foldRight(nel.head)((a, b) => f(a, Now(b)).value)) + } + } + + test("reduce consistent with fold") { + forAll { (nel: NonEmptyList[Int]) => + nel.reduce should === (nel.fold) + } + } + + test("reduce consistent with reduceK") { + forAll { (nel: NonEmptyList[Option[Int]]) => + nel.reduce(SemigroupK[Option].algebra[Int]) should === (nel.reduceK) + } + } + + test("reduceLeftToOption consistent with foldLeft + Option") { + forAll { (nel: NonEmptyList[Int], f: Int => String, g: (String, Int) => String) => + val expected = nel.tail.foldLeft(Option(f(nel.head))) { (opt, i) => + opt.map(s => g(s, i)) + } + nel.reduceLeftToOption(f)(g) should === (expected) + } + } + + test("reduceRightToOption consistent with foldRight + Option") { + forAll { (nel: NonEmptyList[Int], f: Int => String, g: (Int, Eval[String]) => Eval[String]) => + val expected = nel.tail.foldRight(Option(f(nel.head))) { (i, opt) => + opt.map(s => g(i, Now(s)).value) + } + nel.reduceRightToOption(f)(g).value should === (expected) + } + } } diff --git a/tests/src/test/scala/cats/tests/ReducibleTests.scala b/tests/src/test/scala/cats/tests/ReducibleTests.scala new file mode 100644 index 0000000000..d834a99e02 --- /dev/null +++ b/tests/src/test/scala/cats/tests/ReducibleTests.scala @@ -0,0 +1,18 @@ +package cats +package tests + +import cats.data.{NonEmptyList, NonEmptyVector} +import cats.laws.discipline.{ReducibleTests, SerializableTests} +import cats.laws.discipline.arbitrary.oneAndArbitrary + +class ReducibleTest extends CatsSuite { + // Lots of collections here.. telling ScalaCheck to calm down a bit + implicit override val generatorDrivenConfig: PropertyCheckConfiguration = + PropertyCheckConfig(maxSize = 5, minSuccessful = 20) + + type NEVL[A] = NonEmptyList[NonEmptyVector[A]] + val nevlReducible: Reducible[NEVL] = + Reducible[NonEmptyList].compose[NonEmptyVector] + checkAll("NonEmptyList compose NonEmptyVector", ReducibleTests(nevlReducible).reducible[Option, Int, String]) + checkAll("Reducible[NonEmptyList compose NonEmptyVector]", SerializableTests.serializable(nevlReducible)) +}