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

Backport #3103 traverseFilter Queue instance to scala_2.11 #3292

Conversation

gagandeepkalra
Copy link
Contributor

addresses #3143

@gagandeepkalra gagandeepkalra changed the base branch from master to scala_2.11 February 11, 2020 11:50
@gagandeepkalra gagandeepkalra changed the title Backport traverseFilter Queue instance to scala_2.11 Backport traverseFilter Queue instance to scala_2.11 Feb 11, 2020
fa.foldRight(Eval.now(G.pure(Queue.empty[B])))(
(x, xse) => G.map2Eval(f(x), xse)((i, o) => i.fold(o)(_ +: o))
)
.value
Copy link
Contributor Author

@gagandeepkalra gagandeepkalra Feb 11, 2020

Choose a reason for hiding this comment

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

We can have an implementation without using Eval, might even perform a little better (for a bigger queue); without all those extra Eval wrappers.

fa.foldRight(G.pure(Queue.empty[B]))( (x, xse) => G.map2(f(x), xse)((i, o) => i.fold(o)(_ +: o)))

Copy link
Contributor

Choose a reason for hiding this comment

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

That won't short-circuit, though, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@travisbrown yes agreed it won't, but for the same reason the present implementation wouldn't.

If by short circuit you mean f wouldn't have to run on the remaining Queue elements (given we are folding from the right).

This is my understanding and derived reasoning-
map2Eval short-circuits: given the underlying G structure allows it, then the Eval argument skips running.

In this case however, the Eval description is computed by running f on each element of the Queue already. The first argument to map2Eval is not Lazy. In every situation f will run on each element.

Please correct me If I understood this wrongly.

Copy link
Contributor Author

@gagandeepkalra gagandeepkalra Feb 23, 2020

Choose a reason for hiding this comment

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

I see it now, we are using the wrong foldRight here, queue.foldRight ends up with a while loop (standard library). This should have been-

traverse.foldRight[A, G[Queue[B]]](fa, Always(G.pure(Queue.empty))) { (a, lglb) => G.map2Eval(f(a), lglb)((i, o) => i.fold(o)(_ +: o)) } .value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see the same problem with other traverseFilter implementations, should I open a new issue?

@gagandeepkalra gagandeepkalra changed the title Backport traverseFilter Queue instance to scala_2.11 Backport #3103 traverseFilter Queue instance to scala_2.11 Feb 12, 2020
fa.foldRight(Eval.now(G.pure(Queue.empty[B])))(
(x, xse) => G.map2Eval(f(x), xse)((i, o) => i.fold(o)(_ +: o))
)
.value
Copy link
Contributor

Choose a reason for hiding this comment

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

That won't short-circuit, though, right?

@travisbrown
Copy link
Contributor

travisbrown commented Feb 23, 2020

Update

The comment below is wrong. 😄 Or at least the last sentence—it's definitely possible to do that, and @gagandeepkalra's implementation above works:

import cats.Applicative

def traverseFilter[G[_], A, B](fa: Queue[A])(f: (A) => G[Option[B]])(implicit G: Applicative[G]): G[Queue[B]] =
  fa.foldRight(G.pure(Queue.empty[B]))( (x, xse) => G.map2(f(x), xse)((i, o) => i.fold(o)(_ +: o)))

And then:

scala> traverseFilter(Queue(2, 1, 0, -1, -2))(g).value.written
res14: List[Int] = List(2, 1, 0)

I'll look at this more closely tomorrow.

Original comment

@gagandeepkalra Answering your question above here so the discussion doesn't get lost. Part of the problem is that (as far as I know) we don't have very good settled terminology here. If you look at it like this…

import cats.implicits._
import scala.collection.immutable.Queue

val f: Int => Either[String, Option[Int]] = i => {
  print(s"$i ")
  if (i > 0) Right(Some(i)) else Left("not positive")
}

Then right, the implementations don't match—the List one is lazier than the others:

scala> List(2, 1, 0, -1).traverseFilter(f)
2 1 0 res0: Either[String,List[Int]] = Left(not positive)

scala> Vector(2, 1, 0, -1).traverseFilter(f)
-1 0 1 2 res1: Either[String,scala.collection.immutable.Vector[Int]] = Left(not positive)

scala> Queue(2, 1, 0, -1).traverseFilter(f)
-1 0 1 2 res2: Either[String,scala.collection.immutable.Queue[Int]] = Left(not positive)

But that's not what we mean by short-circuiting (or at least that's not how I've been using it). The important thing is that once we've "failed", we stop accumulating the effects of f in G (and may or may not stop applying the function). For example:

import cats.data.{EitherT, Writer}

val g: Int => EitherT[Writer[List[Int], *], String, Option[Int]] = i => if (i > 0) {
  EitherT.right[String](Writer(List(i), Some(i)))
} else {
  EitherT.left(Writer(List(i), "not positive"))
}

And in that sense they all short-circuit as expected:

scala> List(2, 1, 0, -1).traverseFilter(g).value.written
res11: List[Int] = List(2, 1, 0)

scala> Vector(2, 1, 0, -1).traverseFilter(g).value.written
res12: List[Int] = List(2, 1, 0)

scala> Queue(2, 1, 0, -1).traverseFilter(g).value.written
res13: List[Int] = List(2, 1, 0)

I don't think it would be possible to implement traverseFilter without map2Eval (or some equivalent) and maintain this short-circuiting.

@travisbrown
Copy link
Contributor

travisbrown commented Feb 23, 2020

@gagandeepkalra I updated my previous comment because it was completely wrong. 😄

I think you're right and the TraverseFilter implementations for both Queue and Vector (and probably other stuff) are broken. 😦

@gagandeepkalra
Copy link
Contributor Author

gagandeepkalra commented Feb 24, 2020

@travisbrown thank you for following up. I guess we leave this as is here and fix it on the main branch. Would love to take this on though 🙂

@gagandeepkalra
Copy link
Contributor Author

accidentally closed the PR.

@codecov-io
Copy link

codecov-io commented Feb 28, 2020

Codecov Report

❗ No coverage uploaded for pull request base (scala_2.11@0980189). Click here to learn what that means.
The diff coverage is 100%.

Impacted file tree graph

@@              Coverage Diff              @@
##             scala_2.11    #3292   +/-   ##
=============================================
  Coverage              ?   93.47%           
=============================================
  Files                 ?      385           
  Lines                 ?     7174           
  Branches              ?      195           
=============================================
  Hits                  ?     6706           
  Misses                ?      468           
  Partials              ?        0
Impacted Files Coverage Δ
core/src/main/scala/cats/instances/queue.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 0980189...8335f18. Read the comment docs.

@travisbrown travisbrown merged commit f82cd0d into typelevel:scala_2.11 Mar 11, 2020
@gagandeepkalra gagandeepkalra deleted the backport/queueInstances/traverseFilter branch March 11, 2020 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants