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

Issue 3141 #3150

Merged
merged 15 commits into from
Dec 10, 2019
Merged

Issue 3141 #3150

merged 15 commits into from
Dec 10, 2019

Conversation

Twizty
Copy link
Contributor

@Twizty Twizty commented Nov 14, 2019

No description provided.

Copy link
Contributor

@travisbrown travisbrown left a 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] =
Copy link
Contributor

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) =>
Copy link
Contributor

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)?

Copy link
Contributor Author

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`.
Copy link
Contributor

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))
Copy link
Contributor

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)?

@travisbrown
Copy link
Contributor

Oh, just saw the reference to #3141. I think @LukaJCB's idea there is to add A equivalents to M methods—so e.g. foldA would be the applicative implementation of foldM, not fold.

@Twizty Twizty changed the title [WIP] Issue 3141 Issue 3141 Nov 15, 2019
@codecov-io
Copy link

codecov-io commented Nov 15, 2019

Codecov Report

Merging #3150 into master will increase coverage by <.01%.
The diff coverage is 100%.

Impacted file tree graph

@@            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
Flag Coverage Δ
#scala_version_212 93.38% <100%> (ø) ⬆️
#scala_version_213 90.66% <100%> (+0.04%) ⬆️
Impacted Files Coverage Δ
core/src/main/scala/cats/Reducible.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/Foldable.scala 98.5% <100%> (+0.01%) ⬆️
core/src/main/scala/cats/syntax/foldable.scala 84.61% <100%> (+0.61%) ⬆️
core/src/main/scala/cats/syntax/reducible.scala 100% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8867e97...c63b8bc. Read the comment docs.

Copy link
Contributor

@travisbrown travisbrown left a 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.

@Twizty
Copy link
Contributor Author

Twizty commented Nov 18, 2019

@travisbrown Yes, I've seen that comment, and I've forgotten to reply. I tried to implement def foldA[G[_], A, B](fa: F[A], z: B)(f: (B, A) => G[B])(implicit G: Applicative[G]): G[B], but it didn't seem possible, I've asked @LukaJCB, and he said that he expected def foldA[G[_]: Applicative, A: Monoid](fga: F[G[A]]): G[A] and something similar for reduceA. I've implemented reduceMapA similar to reduceA, and foldA, even though it is possible to implement it with like you did or even without using Eval. I can rename foldA and reduceA, and add def reduceMapA[G[_], A, B](fa: F[A])(f: A => G[B])(implicit G: Apply[G], B: Semigroup[B]): G[B]. Would you like me to do that?

@travisbrown
Copy link
Contributor

travisbrown commented Nov 18, 2019

@Twizty @LukaJCB My understanding was that #3141 is about taking methods with a Monad constraint and providing applicative versions (which will sometimes be less performant because they can't rely on tailRecM). This is what foldMapA is in #3130.

I don't think it's a good idea to have a foldM and foldA that do completely different things and have completely different signatures, and I'm not really sure when you'd ever need the F[G[A]] version of foldA provided here, given that we have fold / combineAll. Can you clarify what you were intending, @LukaJCB?

@LukaJCB
Copy link
Member

LukaJCB commented Nov 18, 2019

I was thinking of foldA as an Applicative version of fold AKA combineAll. We already have precedent with MonoidK, foldK and foldMapK.
I figured these identies would hold:

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 foldM would be the same, but turns out it's more like a foldLeftM, which is unfortunate. Maybe we should deprecate it?

@Twizty
Copy link
Contributor Author

Twizty commented Nov 19, 2019

@LukaJCB Is there anything else I need to do on this issue?

LukaJCB
LukaJCB previously approved these changes Nov 19, 2019
@travisbrown
Copy link
Contributor

Sorry but please don't merge this yet—I still have a couple of issues…

@Twizty
Copy link
Contributor Author

Twizty commented Nov 19, 2019

@travisbrown is there anything I need to do?

Copy link
Contributor

@travisbrown travisbrown left a 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 if reduceRightTo 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 for reduceMapM, which I think is a bug. Both foldMapM and foldMapA short-circuit when the underlying operations support it, and it seems like both reduceMap versions should do the same. We can fix reduceMapM in a separate PR, but I don't think we should introduce a broken reduceMapA here.
  • If we're introducing a reduceMapA with almost exactly the same signature and exactly the same semantics as reduceMapM, 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)))
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@travisbrown
Copy link
Contributor

I've addressed the reduceMapM issue here: #3167.

@Twizty
Copy link
Contributor Author

Twizty commented Nov 20, 2019

@travisbrown I've tested your implementation and it also doesn't short-circuit. In my test "Reducible[$name].reduceMapA failure case" I added a log into intToString function:

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 NonEmptyList and NonEmptyVector. foldMapA does short-circuit appropriately. I will fix the short-curcuit bug in my implementation.

@travisbrown
Copy link
Contributor

@Twizty It depends on whether reduceRightTo short-circuits, and some of the current standard library instances don't.

@travisbrown
Copy link
Contributor

@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 foldRight short-circuits then foldMapA/M should as well, and if reduceRightTo short-circuits then reduceMapA/M should.

@Twizty
Copy link
Contributor Author

Twizty commented Nov 20, 2019

@travisbrown that's what I thought. Anyway do I need to do anything or is it better to just close this pull request?

@travisbrown
Copy link
Contributor

travisbrown commented Nov 21, 2019

@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 foldA signature here, but not with either your original implementation or my proposed simplification. 😄

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 foldMapA for the case where we've already mapped, like this:

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 fold, which can't either be lazy or short-circuit. Neither of our implementations supports both of these, but the following does:

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 foldA, if we also deprecate foldM, which seems to be a mistake we got into by copy-pasting Haskell's Control.Monad. 😄

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.

@Twizty
Copy link
Contributor Author

Twizty commented Nov 21, 2019

@travisbrown thank you for such a detailed response! I will fix my implementation then. Do we need reduceA and reduceMapA?

Copy link
Contributor

@travisbrown travisbrown left a 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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants