-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Conversation
Thanks for the PR! Haven't had a chance to look yet, but wanted to link to: |
I think a stack safe and fast 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)
} This isn't tail recursion, but it is constant depth recursion (it goes at most depth 31 since you are using an Int). This produces a tree, so it should be shallow depth for the I would prefer we add this to |
@johnynek Thank you for that, I have edited the code to reflect your suggestion! :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! Can we add a law check replicateA_(n, fa) <-> replicateA(n, fa).void
?
Btw @johnynek is there rationale to make fa: => F[A]
lazy for when n == 0
?
This would also imply that |
test("replicateA_ executes the Applicative action 'fa' 'n' times") { | ||
val A = Applicative[Option] | ||
val fa = A.pure(0) | ||
assert(fa.replicateA_(5) === Option(unit)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a minor bit: can we exercise values other than 5?
Actually, can you make a loop and try all values from 0 up to 10 or something?
+1
I wouldn't because it wouldn't match replicateA and I assume n == 0 is a very rare case. |
@@ -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](fa: F[A]): IsEq[F[Unit]] = | |||
F.replicateA_(2, fa) <-> F.replicateA(2, fa).void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When increasing this number, it causes us to produce large structures in memory when running the tests which caused out of memory errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, even to 3? Even still, I think we should take n: Int
as a parameter to the law, and let the tests decide what number to put there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we tried 5 and explosions resulted - if you think about this it makes sense as if the input data structure that is being replicated is already huge, replicateA(5).void will replicate it fivefold!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you want to plumb this number though the laws? Should these laws accept an explicit parameter now? I don't think that would be binary compatible either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's binary-compatible to add n
as a parameter here, after all we are defining a brand new method! :)
What I'm suggesting is we leave the laws general, and do the hardcoding in the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohh I see, I thought you were suggesting to add this parameter to ApplicativeTests.applicative
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rabinarai1 I think if we can make n
a parameter of the law then this should be good to go.
I think for the tests it would be good to use a value bigger than n = 2
. To keep the structures from getting too large, I wonder if we can use scalacheck tricks like withSize
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@armanbilge we used Gen.resize to resize the generator of F[A]. But even increasing the number to 5 causes out of memory issues.
I've amended the code to address all the comments left :) thank you |
@@ -43,6 +43,7 @@ trait ApplicativeTests[F[_]] extends ApplyTests[F] { | |||
EqFB: Eq[F[B]], | |||
EqFC: Eq[F[C]], | |||
EqFABC: Eq[F[(A, B, C)]], | |||
EqFUnit: Eq[F[Unit]], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh no, I don't think this change is binary-compatible :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we can fix this by deriving:
def EqFUnit(a: A)(implicit Eq[F[A]]): Eq[F[Unit]] = Eq.by(_.as(a))
And then get an A
via Arbitrary[A]
, since we already have Eq[F[A]]
@@ -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 |
There was a problem hiding this comment.
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?
@@ -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](fa: F[A]): IsEq[F[Unit]] = | |||
F.replicateA_(2, fa) <-> F.replicateA(2, fa).void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rabinarai1 I think if we can make n
a parameter of the law then this should be good to go.
I think for the tests it would be good to use a value bigger than n = 2
. To keep the structures from getting too large, I wonder if we can use scalacheck tricks like withSize
.
Needs a |
assert(fa.replicateA_(num) === Option(unit)) | ||
assert(increment.replicateA_(num).runS(0).value === num) | ||
assert(increment.replicateA_(num).run(0).value === ((num, ()))) | ||
assert(increment.replicateA_(num).run(0).value === increment.replicateA(num).void.run(0).value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assert(fa.replicateA_(num) === Option(unit)) | |
assert(increment.replicateA_(num).runS(0).value === num) | |
assert(increment.replicateA_(num).run(0).value === ((num, ()))) | |
assert(increment.replicateA_(num).run(0).value === increment.replicateA(num).void.run(0).value) | |
assertEquals(fa.replicateA_(num), Option(unit)) | |
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) |
@@ -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 | |||
implicit val eqFUnit = makeEqFUnit[A](a) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like the Scala 3 compiler is tripping on this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, looks like we need
implicit val eqFUnit = makeEqFUnit[A](a) | |
implicit val eqFUnit: Eq[F[Unit]] = makeEqFUnit[A](a) |
Huh, this is 2.12 btw.
|
Hmm that is very strange indeed as the same code compiled at c4e960d |
Had a proper look at this and I don't get this compilation error locally - @rabinarai1 how about if we just close and reopen the PR to kick off CI again? If that doesn't work, perhaps it's a conflict that's only introduced on merging into main. |
Aha, I was able to replicate it locally after merging main though 👍 |
@DavidGregory084 thanks for tracking that down! While you are here, feel free to add cats/core/src/main/scala/cats/syntax/parallel.scala Lines 294 to 296 in 7ad08aa
|
Fantastic, thanks!!! 🚀 |
replicateA_
, partReplicateA_
replicateA_
, parReplicateA_
This work was inspired by reading the cats-effect code. We saw that replicateA_ had a TODO message to PR the function to cats, so we decided to try and implement that. We found this difficult to implement in a way that didn't use lots of memory or stack. Then we saw that replicateA_ is missing from other strict functional programming languages like pureScript. So we decided to implement this in the Monad class using tailRecM.
Contributed on behalf of the @opencastsoftware open source team 👨💻