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

override more mapAccumulate methods in Traverse #4214

Merged
merged 5 commits into from
Jun 4, 2022

Conversation

johnynek
Copy link
Contributor

follow up to #4209 to override I think all the remaining values for mapAccumulate.

Also did a few minor changes I noticed along the way.

@johnynek
Copy link
Contributor Author

cc @BalmungSan

@@ -65,6 +65,18 @@ trait IterableInstances {
override def traverse[G[_], A, B](fa: Iterable[A])(f: A => G[B])(implicit G: Applicative[G]): G[Iterable[B]] =
if (fa.isEmpty) G.pure(Iterable.empty)
else G.map(Chain.traverseViaChain(toImIndexedSeq(fa))(f))(_.toVector)

override def mapAccumulate[S, A, B](init: S, fa: Iterable[A])(f: (S, A) => (S, B)): (S, Iterable[B]) = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we can't use the StaticMethod implementation here because we aren't in the cats package.

Copy link
Contributor

Choose a reason for hiding this comment

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

There is also no guarantee that the Iterable would be strict.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point.

override def map[A, B](fa: Map[K, A])(f: A => B): Map[K, B] =
fa.map { case (k, a) => (k, f(a)) }

def foldLeft[A, B](fa: Map[K, A], b: B)(f: (B, A) => B): B =
fa.foldLeft(b) { case (x, (k, a)) => f(x, a) }
fa.foldLeft(b) { case (x, (_, a)) => f(x, a) }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

clear a warning I saw.

@@ -120,6 +120,18 @@ trait SetInstances {
}.value
}

override def mapAccumulate[S, A, B](init: S, fa: Set[A])(f: (S, A) => (S, B)): (S, Set[B]) = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

can't use StaticMethods version outside of cats package.

@@ -213,7 +213,7 @@ class NonEmptyLazyListOps[A](private val value: NonEmptyLazyList[A])
* Tests if some element is contained in this NonEmptyLazyList
*/
final def contains(a: A)(implicit A: Eq[A]): Boolean =
toLazyList.contains(a)
toLazyList.exists(A.eqv(_, a))
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 looks like a straight bug. The contains method was ignoring the A. This is technically changing behavior, but I would argue it is a bug fix.

loop(0).value
}
def traverse[G[_], A, B](fa: ArraySeq[A])(f: A => G[B])(implicit G: Applicative[G]): G[ArraySeq[B]] =
G.map(Chain.traverseViaChain(fa)(f))(_.iterator.to(ArraySeq.untagged))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

make traverse stack safe on ArraySeq. We missed this before.

Copy link
Contributor

Choose a reason for hiding this comment

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

This one is eluding me a bit... How come the previous implementation was not stack safe if each its call to loop is suspended in Eval? Just curious, because I even tried this snippet:

  val arr1 = ArraySeq.iterate(1, 100)(_ + 1)
  val arr2 = arr1.traverse { a =>
    println(s"\n>>> a = $a")
    new Exception().printStackTrace(Console.out)
    a.some
  }

And seems that stack depth is kept as O(1) for the entire traverse evaluation.
Or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does that work when you make 100 something like 100000?

The fn call you are printing is at constant depth but there is more to it, there is also the mapping function on the map2.

That said, we have hit many issues with traverse stack safety in the past and I think the approach used here was the same there (but maybe I am wrong).

Copy link
Contributor

Choose a reason for hiding this comment

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

Does that work when you make 100 something like 100000?

Yes, it does even for 1,000,000 :)

The fn call you are printing is at constant depth but there is more to it, there is also the mapping function on the map2.

That said, we have hit many issues with traverse stack safety in the past and I think the approach used here was the same there (but maybe I am wrong).

I mean I'm not against the new implementation (perhaps it's supposed to be more performant). Just curious, because I tried out and didn't manage to screw up the old one :)

Copy link
Contributor Author

@johnynek johnynek May 31, 2022

Choose a reason for hiding this comment

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

Try with Klesli:

#3960

#3947

That's the issue the motivated the previous changes.

Maybe the real problem was that Kleisli has a bad map2Eval, but it seems you can fix it by avoiding a linear depth in map2Eval.

@@ -225,7 +225,12 @@ sealed abstract class Ior[+A, +B] extends Product with Serializable {
* res2: Option[Int] = Some(123)
* }}}
*/
final def right: Option[B] = fold(_ => None, b => Some(b), (_, b) => Some(b))
final def right: Option[B] =
this match {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

optimization to avoid lambdas when pattern matching will do.

def foldLeft[B, C](fa: A Ior B, b: C)(f: (C, B) => C): C =
fa.foldLeft(b)(f)
def foldRight[B, C](fa: A Ior B, lc: Eval[C])(f: (B, Eval[C]) => Eval[C]): Eval[C] =
fa.foldRight(lc)(f)

override def size[B](fa: A Ior B): Long = fa.fold(_ => 0L, _ => 1L, (_, _) => 1L)
override def size[B](fa: A Ior B): Long =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

optimization to avoid lambdas when pattern matching will do.

Copy link
Member

Choose a reason for hiding this comment

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

How about simply if (fa.isLeft) 0L else 1L.

Copy link
Member

@armanbilge armanbilge Jun 4, 2022

Choose a reason for hiding this comment

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

Oh, yikes, isLeft and friends need to be optimized too (maybe by dynamic dispatch which is how Option is implemented nope, was wrong about Option).

Regardless, I still think this method would be better written in terms of isLeft.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

BalmungSan
BalmungSan previously approved these changes May 28, 2022
@@ -65,6 +65,18 @@ trait IterableInstances {
override def traverse[G[_], A, B](fa: Iterable[A])(f: A => G[B])(implicit G: Applicative[G]): G[Iterable[B]] =
if (fa.isEmpty) G.pure(Iterable.empty)
else G.map(Chain.traverseViaChain(toImIndexedSeq(fa))(f))(_.toVector)

override def mapAccumulate[S, A, B](init: S, fa: Iterable[A])(f: (S, A) => (S, B)): (S, Iterable[B]) = {
Copy link
Contributor

Choose a reason for hiding this comment

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

What about also adding mapWithIndex and zipWithIndex here?

override def mapWithIndex[A, B](fa: Iterable[A])(f: (A, Int) => B): Iterable[B] =
  fa.zipWithIndex.map(ai => f(ai._1, ai._2))

override def zipWithIndex[A](fa: Iterable[A]): Iterable[(A, Int)] =
  fa.zipWithIndex

The motivation being they would preserve their underling class.

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 made this change but I'm suspect about the trying to add invariants we don't really promise (returning the same underlying type).

  1. this is already in alleycats where there are reasons to believe things are suspect. In this case Iterable is so weak it doesn't necessarily iterate in any meaningful order (otherwise it would be Seq).
  2. when people start to assume we offer some stronger contracts, they often wind up getting into trouble and file issues and then we are in a jam. For instance, mapWithIndex is exercising a different code path from mapAccumulate here, and if they want some other law we don't promise (like maybe they do a cast at the end because they assume the Iterable is of the same type), then we get into weird issues of explaining why we went out of our way to maintain the class, but sometimes fail to.

anyway, it's alleycats, so I added it. But I hope people won't rely on it.

@johnynek
Copy link
Contributor Author

@armanbilge can you review this?

@armanbilge armanbilge self-requested a review May 31, 2022 22:24
danicheg
danicheg previously approved these changes Jun 2, 2022
Copy link
Member

@danicheg danicheg left a comment

Choose a reason for hiding this comment

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

LGTM.

override def mapAccumulate[S, A, B](init: S, fa: Map[K, A])(f: (S, A) => (S, B)): (S, Map[K, B]) = {
val iter = fa.iterator
var s = init
val m = Map.newBuilder[K, B]
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about adding the call of m.sizeHint? I haven't a strong opinion it's required, considering extra computing of Map length, but someone used it in cats.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good call. I'll update that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

Copy link
Member

@armanbilge armanbilge left a comment

Choose a reason for hiding this comment

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

Thanks, LGTM!

@johnynek johnynek merged commit 1346a0e into main Jun 4, 2022
@armanbilge armanbilge deleted the oscar/20220528_more_mapAccumulate branch June 4, 2022 23:41
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.

5 participants