-
-
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
Issue 3141 #3150
Issue 3141 #3150
Conversation
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 these additions need more motivation—it seems like passing Applicative.monoid
or Apply.semigroup
explicitly as needed isn't that bad.
* res0: Right[Int] = Right(3) | ||
* }}} | ||
*/ | ||
def foldA[G[_], A](fga: F[G[A]])(implicit G: Applicative[G], A: Monoid[A]): G[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.
I'm not sure about the name here, and I don't think we want to set a precedent for adding methods that just replace a A: Monoid
argument with a G[A]
+ G: Applicative[G], A: Monoid
argument, or at least not without a clear case that doing so makes some common usage much more convenient.
* }}} | ||
*/ | ||
def foldA[G[_], A](fga: F[G[A]])(implicit G: Applicative[G], A: Monoid[A]): G[A] = | ||
foldLeft(fga, G.pure(A.empty)) { (acc, ga) => |
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 a reason to prefer this implementation over fold(fga)(G.monoid)
?
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.
This is better, I didn't know about this function.
* Reduce a `F[G[A]]` value using `Applicative[G]` and `Semigroup[A]`, a universal | ||
* semigroup for `G[_]`. | ||
* | ||
* This method is a generalization of `reduce`. |
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'm not sure about "generalization" here—in what sense is the reduce
signature less general?
Also, couldn't the constraint both here and below be Apply
instead of Applicative
?
* This method is a generalization of `reduce`. | ||
*/ | ||
def reduceA[G[_], A](fga: F[G[A]])(implicit G: Applicative[G], A: Semigroup[A]): G[A] = | ||
reduceLeft(fga)((ga1, ga2) => G.map2(ga1, ga2)(A.combine)) |
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.
Why not reduce(fga)(Apply[G].semigroup)
?
Codecov Report
@@ Coverage Diff @@
## master #3150 +/- ##
==========================================
+ Coverage 93.04% 93.04% +<.01%
==========================================
Files 376 376
Lines 7374 7379 +5
Branches 209 208 -1
==========================================
+ Hits 6861 6866 +5
Misses 513 513
Continue to review full report at Codecov.
|
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.
@Twizty Did you see my comment above? These methods don't have the right signatures for #3141. Take reduceMapM
:
def reduceMapM[G[_], A, B](fa: F[A])(f: A => G[B])(implicit G: FlatMap[G], B: Semigroup[B]): G[B]
The applicative version of this would be something like:
def reduceMapA[G[_], A, B](fa: F[A])(f: A => G[B])(implicit G: Apply[G], B: Semigroup[B]): G[B] =
reduceRightTo(fa)(f)((a, egb) => G.map2Eval(f(a), egb)(B.combine)).value
Not:
def reduceMapA[G[_], A, B](fga: F[G[A]])(f: A => B)(implicit G: Apply[G], B: Semigroup[B]): G[B]
I'm not entirely sure what @LukaJCB had in mind for foldA
and reduceA
—I'm guessing something like this for foldA
, on the analogy of foldMapA
in #3130 or my reduceMapA
above:
def foldA[G[_], A, B](fa: F[A], z: B)(f: (B, A) => G[B])(implicit G: Applicative[G]): G[B]
But at a glance I don't think it's possible to implement this. In any case the version you have doesn't really seem like it should be called foldA
—it's more like a less generic combineAll
.
@travisbrown Yes, I've seen that comment, and I've forgotten to reply. I tried to implement |
@Twizty @LukaJCB My understanding was that #3141 is about taking methods with a I don't think it's a good idea to have a |
I was thinking of fa.foldMap(identity) === fa.fold
fa.foldMapK(identity) === fa.foldK
fa.reduceMap(identity) === fa.reduce
fa.reduceMapK(identity) === fa.reduceK
// this one is the outlier
fa.foldMapM(identity) === fa.foldM I assumed |
@LukaJCB Is there anything else I need to do on this issue? |
Sorry but please don't merge this yet—I still have a couple of issues… |
@travisbrown is there anything I need to do? |
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.
Sorry, got distracted by the bincompat breakage fixes in #3162 and #3163 yesterday.
There are two things I think still need to be addressed for reduceMapA
:
- The first is that the implementation you've given of
reduceMapA
here won't short-circuit even ifreduceRightTo
is sufficiently lazy, while the implementation I've given in a comment above will short-circuit appropriately. I just noticed that this is also the case forreduceMapM
, which I think is a bug. BothfoldMapM
andfoldMapA
short-circuit when the underlying operations support it, and it seems like bothreduceMap
versions should do the same. We can fixreduceMapM
in a separate PR, but I don't think we should introduce a brokenreduceMapA
here. - If we're introducing a
reduceMapA
with almost exactly the same signature and exactly the same semantics asreduceMapM
, I think we need to explain the apparent redundancy in the API docs, similar to what we did in add foldMapA as an alternative to foldMapM that only requires an Applicative rather than a monad #3130.
I have to admit I also still just totally don't understand why we need foldA
or reduceA
, or how e.g. a foldA
with the signature here is "an applicative version of fold
".
We currently have the following:
def fold[A](fa: F[A])(implicit A: Monoid[A]): A
def foldM[G[_], A, B](fa: F[A], z: B)(f: (B, A) => G[B])(implicit G: Monad[G]): G[B]
And this PR adds:
def foldA[G[_], A](fga: F[G[A]])(implicit G: Applicative[G], A: Monoid[A]): G[A]
I think there are two problems with this. This first is that by analogy with foldMapA/M
, etc., I'd expect foldA
to look like this:
def foldA[G[_], A, B](fa: F[A], z: B)(f: (B, A) => G[B])(implicit G: Applicative[G]): G[B]
But it's not possible to implement this in general for Foldable
. I think that should rule out having a method named foldA
at all because of the potential confusion, but even assuming we were okay with having a foldA
, I'm just not sure what potential use cases there are for this method:
def foldA[G[_], A](fga: F[G[A]])(implicit G: Applicative[G], A: Monoid[A]): G[A]
It feels entirely redundant with fold
. If we have a Applicative[G]
and a Monoid[A]
, then in every case I can think of we'll also have a Monoid[G[A]]
, and we can just use fold
. Even if we don't have a Monoid[G[A]]
, we can trivially construct one with Applicative.monoid[G, A]
.
@@ -29,5 +29,9 @@ final class ReducibleOps0[F[_], A](private val fa: F[A]) extends AnyVal { | |||
* a: String = "foo321" | |||
* }}} | |||
* */ | |||
def reduceMapK[G[_], B](f: A => G[B])(implicit F: Reducible[F], G: SemigroupK[G]): G[B] = F.reduceMapK[G, A, B](fa)(f) | |||
def reduceMapK[G[_], B](f: A => G[B])(implicit F: Reducible[F], G: SemigroupK[G]): G[B] = | |||
F.reduceLeftTo(fa)(f)((b, a) => G.combineK(b, f(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.
Why did you change this? In general we want to avoid having implementation logic in syntax type classes.
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.
Yes, my bad, I'll remove this diff.
I've addressed the |
@travisbrown I've tested your implementation and it also doesn't short-circuit. In my test def intToString(i: Long): Either[String, Int] = {
println(("!!!", i))
if (i == 2) i.toInt.asRight else "boom!!!".asLeft
} It prints all the numbers from 1 to 3 for |
@Twizty It depends on whether |
@Twizty The idea is that default implementations in the type class shouldn't undo the short-circuiting behavior of the abstract type class operation—i.e. if |
@travisbrown that's what I thought. Anyway do I need to do anything or is it better to just close this pull request? |
@Twizty I was thinking about this more last night and I was wrong: there is actually a reasonable use case for a method with the Suppose we want to sum up numbers in a list as long as they're all positive, but want to fail if we see zero or a negative number, and we want to log the positive numbers we successfully added: import cats.Foldable, cats.data.{EitherT, Writer}, cats.implicits._
type W[A] = Writer[List[Int], A]
def checkPos(x: Int): EitherT[W, Int, Int] =
if (x > 0) EitherT.right(Writer(List(x), x)) else EitherT.leftT(x) Then we can do something like this: scala> List(1, 2, 3, 4).foldMapA(checkPos)
res0: cats.data.EitherT[W,Int,Int] = EitherT(WriterT((List(1, 2, 3, 4),Right(10))))
scala> List(1, 2, 3, 4, 0, 5).foldMapA(checkPos)
res1: cats.data.EitherT[W,Int,Int] = EitherT(WriterT((List(1, 2, 3, 4),Left(0)))) Or: scala> Foldable[List].fold(List(1, 2, 3, 4).map(checkPos))
res4: cats.data.EitherT[W,Int,Int] = EitherT(WriterT((List(1, 2, 3, 4),Right(10))))
scala> Foldable[List].fold(List(1, 2, 3, 4, 0, 5).map(checkPos))
res5: cats.data.EitherT[W,Int,Int] = EitherT(WriterT((List(1, 2, 3, 4, 5),Left(0)))) But we might want the short-circuiting behavior of scala> Foldable[List].foldA(List(1, 2, 3, 4, 0, 5).map(checkPos))
res6: cats.data.EitherT[W,Int,Int] = EitherT(WriterT((List(1, 2, 3, 4),Left(0)))) We might also want this to be lazy: scala> import cats.Foldable, cats.implicits._
import cats.Foldable
import cats.implicits._
scala> def checkPos(x: Int): Either[Int, Int] = if (x > 0) Right(x) else Left(x)
checkPos: (x: Int)Either[Int,Int]
scala> val s = (Stream(1, 2, 3, 0) ++ Stream.from(5))
s: scala.collection.immutable.Stream[Int] = Stream(1, ?)
scala> Foldable[Stream].foldA(s.map(checkPos))
res0: Either[Int,Int] = Left(0) In both cases the behavior is different from def foldA[G[_], A](fga: F[G[A]])(implicit G: Applicative[G], A: Monoid[A]): G[A] =
foldMapA(fga)(identity) So I'm convinced now we should have this method, and even that we could probably call it We also definitely need to explain all of this clearly in the API docs. Again, sorry to drag out this PR, but it's turned up a really interesting set of questions and problems with the current state of this stuff—thanks for helping to push this forward. |
@travisbrown thank you for such a detailed response! I will fix my implementation then. Do we need |
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'm working on a follow-up PR right now, but I think we can go ahead and merge this one. @LukaJCB?
No description provided.