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 replicateA_, parReplicateA_ #4208

Merged
merged 11 commits into from
Jun 9, 2022
29 changes: 29 additions & 0 deletions core/src/main/scala/cats/Applicative.scala
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,35 @@ trait Applicative[F[_]] extends Apply[F] with InvariantMonoidal[F] { self =>
})(identity)(this))(_.toList)
}

/**
* Given `fa` and `n`, apply `fa` `n` times discarding results to return F[Unit].
*
* Example:
* {{{
* scala> import cats.data.State
*
* scala> type Counter[A] = State[Int, A]
* scala> val getAndIncrement: Counter[Int] = State { i => (i + 1, i) }
* scala> val getAndIncrement5: Counter[Unit] =
* | Applicative[Counter].replicateA_(5, getAndIncrement)
* scala> getAndIncrement5.run(0).value
* res0: (Int, Unit) = (5,())
* }}}
*/
def replicateA_[A](n: Int, fa: F[A]): F[Unit] = {
val fvoid = void(fa)
def loop(n: Int): F[Unit] =
if (n <= 0) unit
else if (n == 1) fvoid
else {
val half = loop(n >> 1)
val both = productR(half)(half)
if ((n & 1) == 1) productR(both)(fvoid)
else both
}
loop(n)
}

/**
* Compose an `Applicative[F]` and an `Applicative[G]` into an
* `Applicative[λ[α => F[G[α]]]]`.
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/scala/cats/Parallel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,13 @@ object Parallel extends ParallelArityFunctions2 {
def parReplicateA[M[_], A](n: Int, ma: M[A])(implicit P: Parallel[M]): M[List[A]] =
P.sequential(P.applicative.replicateA(n, P.parallel(ma)))

/**
* Like `Applicative[F].replicateA_`, but uses the apply instance
* corresponding to the Parallel instance instead.
*/
def parReplicateA_[M[_], A](n: Int, ma: M[A])(implicit P: Parallel[M]): M[Unit] =
P.sequential(P.applicative.replicateA_(n, P.parallel(ma)))

/**
* Provides an `ApplicativeError[F, E]` instance for any F, that has a `Parallel.Aux[M, F]`
* and a `MonadError[M, E]` instance.
Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/cats/syntax/applicative.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ final class ApplicativeOps[F[_], A](private val fa: F[A]) extends AnyVal {

final class ApplicativeByValueOps[F[_], A](private val fa: F[A]) extends AnyVal {
def replicateA(n: Int)(implicit F: Applicative[F]): F[List[A]] = F.replicateA(n, fa)
def replicateA_(n: Int)(implicit F: Applicative[F]): F[Unit] = F.replicateA_(n, fa)
}

final class ApplicativeByNameOps[F[_], A](private val fa: () => F[A]) extends AnyVal {
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/scala/cats/syntax/parallel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,16 @@ final class ParallelApOps[M[_], A](private val ma: M[A]) extends AnyVal {

def parReplicateA(n: Int)(implicit P: Parallel[M]): M[List[A]] =
Parallel.parReplicateA(n, ma)

def parReplicateA_(n: Int)(implicit P: Parallel[M]): M[Unit] =
Parallel.parReplicateA_(n, ma)
}

final class ParallelApOps1[M[_], A](private val ma: M[A]) extends AnyVal {
def parReplicateA(n: Int)(implicit P: Parallel[M]): M[List[A]] =
Parallel.parReplicateA(n, ma)
def parReplicateA_(n: Int)(implicit P: Parallel[M]): M[Unit] =
Parallel.parReplicateA_(n, ma)
}

final class NonEmptyParallelApOps[M[_], A](private val ma: M[A]) extends AnyVal {
Expand Down
3 changes: 3 additions & 0 deletions laws/src/main/scala/cats/laws/ApplicativeLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ trait ApplicativeLaws[F[_]] extends ApplyLaws[F] {
def applicativeUnit[A](a: A): IsEq[F[A]] =
F.unit.map(_ => a) <-> F.pure(a)

def replicateAVoidReplicateA_Consistent[A](n: Int, fa: F[A]): IsEq[F[Unit]] =
F.replicateA_(n, fa) <-> F.replicateA(n, fa).void

// The following are the lax monoidal functor identity laws - the associativity law is covered by
// Semigroupal's associativity law.

Expand Down
15 changes: 12 additions & 3 deletions laws/src/main/scala/cats/laws/discipline/ApplicativeTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ package laws
package discipline

import cats.laws.discipline.SemigroupalTests.Isomorphisms
import org.scalacheck.{Arbitrary, Cogen, Prop}
import Prop._
import org.scalacheck.Prop._
import org.scalacheck.{Arbitrary, Cogen, Gen}

trait ApplicativeTests[F[_]] extends ApplyTests[F] {
def laws: ApplicativeLaws[F]

private def makeEqFUnit[A](a: A)(implicit EqFA: Eq[F[A]]): Eq[F[Unit]] =
Eq.by(fa => laws.F.as(fa, a))

def applicative[A: Arbitrary, B: Arbitrary, C: Arbitrary](implicit
ArbFA: Arbitrary[F[A]],
ArbFB: Arbitrary[F[B]],
Expand All @@ -44,7 +47,7 @@ trait ApplicativeTests[F[_]] extends ApplyTests[F] {
EqFC: Eq[F[C]],
EqFABC: Eq[F[(A, B, C)]],
iso: Isomorphisms[F]
): RuleSet =
): RuleSet = {
new DefaultRuleSet(
name = "applicative",
parent = Some(apply[A, B, C]),
Expand All @@ -54,9 +57,15 @@ trait ApplicativeTests[F[_]] extends ApplyTests[F] {
"applicative map" -> forAll(laws.applicativeMap[A, B] _),
"applicative unit" -> forAll(laws.applicativeUnit[A] _),
"ap consistent with product + map" -> forAll(laws.apProductConsistent[A, B] _),
"replicateA_ consistent with replicateA.void" -> forAll { (a: A) =>
// Should be an implicit parameter but that is not a binary-compatible change
Copy link
Member

Choose a reason for hiding this comment

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

Is there some place that these bincompat hacks are being catalogued? Should we make this a // TODO: cats 3.x comment?

implicit val eqFUnit: Eq[F[Unit]] = makeEqFUnit[A](a)
forAll(Gen.resize(4, ArbFA.arbitrary))(laws.replicateAVoidReplicateA_Consistent[A](4, _))
},
"monoidal left identity" -> forAll((fa: F[A]) => iso.leftIdentity(laws.monoidalLeftIdentity(fa))),
"monoidal right identity" -> forAll((fa: F[A]) => iso.rightIdentity(laws.monoidalRightIdentity(fa)))
)
}
}

object ApplicativeTests {
Expand Down
19 changes: 17 additions & 2 deletions tests/shared/src/test/scala/cats/tests/ApplicativeSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@

package cats.tests

import cats.{Align, Applicative, Apply, CoflatMap}
import cats.data.{Const, Validated}
import cats.data.{Const, State, Validated}
import cats.kernel.Monoid
import cats.kernel.laws.discipline.{MonoidTests, SemigroupTests}
import cats.laws.discipline.arbitrary._
import cats.laws.discipline.{AlignTests, CoflatMapTests}
import cats.syntax.applicative._
import cats.syntax.eq._
import cats.syntax.functor._
import cats.{Align, Applicative, Apply, CoflatMap}
import org.scalacheck.Prop._

class ApplicativeSuite extends CatsSuite {
Expand All @@ -39,6 +40,20 @@ class ApplicativeSuite extends CatsSuite {
assert(fa.replicateA(5) === (Some(List(1, 1, 1, 1, 1))))
}

test("replicateA_ executes the Applicative action 'fa' 'n' times") {
val A = Applicative[Option]
val fa = A.pure(0)
val increment: State[Int, Int] = State { i => (i + 1, i) }
val aUnit = A.unit

for (num <- 0 to 10) {
assertEquals(fa.replicateA_(num), aUnit)
assertEquals(increment.replicateA_(num).runS(0).value, num)
assertEquals(increment.replicateA_(num).run(0).value, ((num, ())))
assertEquals(increment.replicateA_(num).run(0).value, increment.replicateA(num).void.run(0).value)
}
}

test("whenA return given argument when cond is true") {
forAll { (l: List[Int]) =>
assert(l.whenA(true) === (List.fill(l.length)(())))
Expand Down
6 changes: 3 additions & 3 deletions tests/shared/src/test/scala/cats/tests/MonadSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@

package cats.tests

import cats.{Eval, Id, Monad}
import cats.data.{IndexedStateT, StateT}
import cats.syntax.apply._
import cats.syntax.monad._
import org.scalacheck.{Arbitrary, Gen}
import cats.syntax.eq._
import cats.syntax.monad._
import cats.{Eval, Id, Monad}
import org.scalacheck.Prop._
import org.scalacheck.{Arbitrary, Gen}

class MonadSuite extends CatsSuite {
implicit val testInstance: Monad[StateT[Id, Int, *]] = IndexedStateT.catsDataMonadForIndexedStateT[Id, Int]
Expand Down
13 changes: 13 additions & 0 deletions tests/shared/src/test/scala/cats/tests/ParallelSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,25 @@ class ParallelSuite
}
}

test("ParReplicateA_ should be equivalent to fill parSequence_") {
forAll(Gen.choose(1, 20), Arbitrary.arbitrary[Either[String, String]]) {
(repetitions: Int, e: Either[String, String]) =>
assert(Parallel.parReplicateA_(repetitions, e) === Parallel.parSequence_(List.fill(repetitions)(e)))
}
}

test("ParReplicateA 2 should be equivalent to parMap2 List") {
forAll { (e: Either[String, String]) =>
assert(Parallel.parReplicateA(2, e) === Parallel.parMap2(e, e)((s1, s2) => List(s1, s2)))
}
}

test("ParReplicateA_ 2 should be equivalent to parMap2.void List") {
forAll { (e: Either[String, String]) =>
assert(Parallel.parReplicateA_(2, e) === Parallel.parMap2(e, e)((s1, s2) => List(s1, s2)).void)
}
}

test("Kleisli with Either should accumulate errors") {
val k1: Kleisli[Either[String, *], String, Int] = Kleisli(s => Right(s.length))
val k2: Kleisli[Either[String, *], String, Int] = Kleisli(s => Left("Boo"))
Expand Down
3 changes: 3 additions & 0 deletions tests/shared/src/test/scala/cats/tests/SyntaxSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ object SyntaxSuite {
val ma = mock[M[A]]

val mla: M[List[A]] = ma.parReplicateA(mock[Int])
val mu: M[Unit] = ma.parReplicateA_(mock[Int])
}

def testNonEmptyParallel[M[_]: NonEmptyParallel, A, B]: Unit = {
Expand Down Expand Up @@ -415,6 +416,8 @@ object SyntaxSuite {
def testApplicative[F[_]: Applicative, A]: Unit = {
val a = mock[A]
val fa = a.pure[F]
val replicateA = fa.replicateA(1)
val replicateA_ = fa.replicateA_(1)
}

def testFlatMap[F[_]: FlatMap, A, B, C, D, Z]: Unit = {
Expand Down