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

restrict traverse_ and friends to require Unit #4352

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 16 additions & 21 deletions core/src/main/scala/cats/Foldable.scala
Expand Up @@ -569,50 +569,45 @@ trait Foldable[F[_]] extends UnorderedFoldable[F] with FoldableNFunctions[F] { s
/**
* Traverse `F[A]` using `Applicative[G]`.
*
* `A` values will be mapped into `G[B]` and combined using
* `A` values will be mapped into `G[Unit]` and combined using
* `Applicative#map2`.
*
* For example:
*
* {{{
* scala> import cats.implicits._
* scala> def parseInt(s: String): Option[Int] = Either.catchOnly[NumberFormatException](s.toInt).toOption
* scala> def checkInt(s: String): Option[Unit] = Either.catchOnly[NumberFormatException]{ s.toInt ; () }.toOption
* scala> val F = Foldable[List]
* scala> F.traverse_(List("333", "444"))(parseInt)
* scala> F.traverse_(List("333", "444"))(checkInt)
* res0: Option[Unit] = Some(())
* scala> F.traverse_(List("333", "zzz"))(parseInt)
* scala> F.traverse_(List("333", "zzz"))(checkInt)
* res1: Option[Unit] = None
* }}}
*
* This method is primarily useful when `G[_]` represents an action
* or effect, and the specific `A` aspect of `G[A]` is not otherwise
* needed.
* or effect. It is equivalent to `foldMapA` using the Unit monoid.
*/
def traverse_[G[_], A, B](fa: F[A])(f: A => G[B])(implicit G: Applicative[G]): G[Unit] =
foldRight(fa, Always(G.pure(()))) { (a, acc) =>
G.map2Eval(f(a), acc) { (_, _) =>
()
}
}.value
def traverse_[G[_], A](fa: F[A])(f: A => G[Unit])(implicit G: Applicative[G]): G[Unit] =
foldMapA(fa)(f)
Copy link
Member

Choose a reason for hiding this comment

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

I'm afraid this change is not compatible. Although we can change the signature, we need to keep the current implementation. See the test I added in 41439fc.

sbt:root> binCompatTest/test
catsBC.MimaExceptionsTest:
==> X catsBC.MimaExceptionsTest.is binary compatible  0.566s java.lang.ClassCastException: class java.lang.String cannot be cast to class scala.runtime.BoxedUnit (java.lang.String is in module java.base of loader 'bootstrap'; scala.runtime.BoxedUnit is in unnamed module of loader sbt.internal.ScalaLibraryClassLoader @15aec8f1)
    at cats.instances.EitherInstances$$anon$2.$anonfun$map2Eval$2(either.scala:95)
    at scala.util.Either.map(Either.scala:382)
    at cats.instances.EitherInstances$$anon$2.$anonfun$map2Eval$1(either.scala:95)
    at cats.Eval.$anonfun$map$1(Eval.scala:78)
    at cats.Eval$.loop$1(Eval.scala:379)
    at cats.Eval$.cats$Eval$$evaluate(Eval.scala:384)
    at cats.Eval$FlatMap.value(Eval.scala:305)
    at cats.instances.ListInstances$$anon$1.traverse_(list.scala:163)
    at cats.instances.ListInstances$$anon$1.traverse_(list.scala:37)
    at cats.Foldable$Ops.traverse_(Foldable.scala:1050)
    at cats.Foldable$Ops.traverse_$(Foldable.scala:1049)
    at cats.Foldable$ToFoldableOps$$anon$6.traverse_(Foldable.scala:1075)
    at catsBC.MimaExceptions$.isBinaryCompatible(MimaExceptions.scala:58)
    at catsBC.MimaExceptionsTest.$anonfun$new$1(MimaExceptionsTest.scala:28)

Copy link
Member

Choose a reason for hiding this comment

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

Or possibly, this implementation could work:

foldMapA(fa)(a => G.void(f(a)))

Copy link
Author

Choose a reason for hiding this comment

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

yikes -- neither G.void nor even the original implementation seems to make this go away, at least on my local machine. Does that mean the change can't be made with binary compatibility at all?

Copy link
Member

Choose a reason for hiding this comment

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

Damn, really? I thought it should be possible, if done carefully. But maybe not.

Also, maybe it's not worth the risk since clearly this is not easy to reason about.

Copy link
Author

@jpassaro jpassaro Nov 20, 2022

Choose a reason for hiding this comment

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

pushed my branch with (credited) breaking test, and attempt to fix it by restoring original implementations. It didn't work. Wondering if:

a) there is some path to binary compatibility that I don't understand;
b) is it better to add new functions (traverseEach, sequenceEach, maybe traverseEachM for monads) and possibly deprecate the unrestricted variants (traverse_ and sequence_)?

happy to proceed however y'all think best. many thanks @armanbilge for spotting this issue

Copy link
Contributor

Choose a reason for hiding this comment

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

btw: what is the motivation for making this change?

foldMapA is relatively rarely used, I think. While we have worked out many stack overflow issues around traverse and sequence. Touching these implementations has an excellent chance of introducing stack overflow errors for users I think.

Copy link
Author

Choose a reason for hiding this comment

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

the motivation was that the implementation is identical so why not reduce repetition; but since it broke binary compatibility (see Arman's added MIMA test, now added to the PR) I reverted it.


/**
* Sequence `F[G[A]]` using `Applicative[G]`.
* Sequence `F[G[Unit]]` using `Applicative[G]`.
*
* This is similar to `traverse_` except it operates on `F[G[A]]`
* This is similar to `traverse_` except it operates on `F[G[Unit]]`
* values, so no additional functions are needed.
*
* For example:
*
* {{{
* scala> import cats.implicits._
* scala> val F = Foldable[List]
* scala> F.sequence_(List(Option(1), Option(2), Option(3)))
* scala> F.sequence_(List(Option(()), Option(()), Option(())))
* res0: Option[Unit] = Some(())
* scala> F.sequence_(List(Option(1), None, Option(3)))
* scala> F.sequence_(List(Option(()), None, Option(())))
* res1: Option[Unit] = None
* }}}
*/
def sequence_[G[_]: Applicative, A](fga: F[G[A]]): G[Unit] =
def sequence_[G[_]: Applicative](fga: F[G[Unit]]): G[Unit] =
traverse_(fga)(identity)

/**
Expand Down Expand Up @@ -1051,10 +1046,10 @@ object Foldable {
typeClassInstance.foldMapM[G, A, B](self)(f)(G, B)
def foldMapA[G[_], B](f: A => G[B])(implicit G: Applicative[G], B: Monoid[B]): G[B] =
typeClassInstance.foldMapA[G, A, B](self)(f)(G, B)
def traverse_[G[_], B](f: A => G[B])(implicit G: Applicative[G]): G[Unit] =
typeClassInstance.traverse_[G, A, B](self)(f)(G)
def sequence_[G[_], B](implicit ev$1: A <:< G[B], ev$2: Applicative[G]): G[Unit] =
typeClassInstance.sequence_[G, B](self.asInstanceOf[F[G[B]]])
def traverse_[G[_]](f: A => G[Unit])(implicit G: Applicative[G]): G[Unit] =
typeClassInstance.traverse_[G, A](self)(f)(G)
def sequence_[G[_]](implicit ev$1: A <:< G[Unit], ev$2: Applicative[G]): G[Unit] =
typeClassInstance.sequence_[G](self.asInstanceOf[F[G[Unit]]])
def foldK[G[_], B](implicit ev$1: A <:< G[B], G: MonoidK[G]): G[B] =
typeClassInstance.foldK[G, B](self.asInstanceOf[F[G[B]]])(G)
def find(f: A => Boolean): Option[A] = typeClassInstance.find[A](self)(f)
Expand Down
14 changes: 7 additions & 7 deletions core/src/main/scala/cats/Parallel.scala
Expand Up @@ -263,7 +263,7 @@ object Parallel extends ParallelArityFunctions2 {
* Like `Foldable[A].sequence_`, but uses the applicative instance
* corresponding to the Parallel instance instead.
*/
def parSequence_[T[_]: Foldable, M[_], A](tma: T[M[A]])(implicit P: Parallel[M]): M[Unit] = {
def parSequence_[T[_]: Foldable, M[_]](tma: T[M[Unit]])(implicit P: Parallel[M]): M[Unit] = {
val fu: P.F[Unit] = Foldable[T].traverse_(tma)(P.parallel.apply(_))(P.applicative)
P.sequential(fu)
}
Expand All @@ -272,9 +272,9 @@ object Parallel extends ParallelArityFunctions2 {
* Like `Foldable[A].traverse_`, but uses the applicative instance
* corresponding to the Parallel instance instead.
*/
def parTraverse_[T[_]: Foldable, M[_], A, B](
def parTraverse_[T[_]: Foldable, M[_], A](
ta: T[A]
)(f: A => M[B])(implicit P: Parallel[M]): M[Unit] = {
)(f: A => M[Unit])(implicit P: Parallel[M]): M[Unit] = {
val gtb: P.F[Unit] = Foldable[T].traverse_(ta)(a => P.parallel(f(a)))(P.applicative)
P.sequential(gtb)
}
Expand Down Expand Up @@ -348,8 +348,8 @@ object Parallel extends ParallelArityFunctions2 {
* Like `Reducible[A].nonEmptySequence_`, but uses the apply instance
* corresponding to the Parallel instance instead.
*/
def parNonEmptySequence_[T[_]: Reducible, M[_], A](
tma: T[M[A]]
def parNonEmptySequence_[T[_]: Reducible, M[_]](
tma: T[M[Unit]]
)(implicit P: NonEmptyParallel[M]): M[Unit] = {
val fu: P.F[Unit] = Reducible[T].nonEmptyTraverse_(tma)(P.parallel.apply(_))(P.apply)
P.sequential(fu)
Expand All @@ -359,9 +359,9 @@ object Parallel extends ParallelArityFunctions2 {
* Like `Reducible[A].nonEmptyTraverse_`, but uses the apply instance
* corresponding to the Parallel instance instead.
*/
def parNonEmptyTraverse_[T[_]: Reducible, M[_], A, B](
def parNonEmptyTraverse_[T[_]: Reducible, M[_], A](
ta: T[A]
)(f: A => M[B])(implicit P: NonEmptyParallel[M]): M[Unit] = {
)(f: A => M[Unit])(implicit P: NonEmptyParallel[M]): M[Unit] = {
val gtb: P.F[Unit] = Reducible[T].nonEmptyTraverse_(ta)(a => P.parallel(f(a)))(P.apply)
P.sequential(gtb)
}
Expand Down
18 changes: 8 additions & 10 deletions core/src/main/scala/cats/Reducible.scala
Expand Up @@ -200,7 +200,7 @@ trait Reducible[F[_]] extends Foldable[F] { self =>
* Traverse `F[A]` using `Apply[G]`.
*
* `A` values will be mapped into `G[B]` and combined using
* `Apply#map2`.
* `Apply#map2`. This is equivalent to `reduceMapA`.
*
* This method is similar to [[Foldable.traverse_]]. There are two
* main differences:
Expand All @@ -213,10 +213,8 @@ trait Reducible[F[_]] extends Foldable[F] { self =>
* available for `G` and want to take advantage of short-circuiting
* the traversal.
*/
def nonEmptyTraverse_[G[_], A, B](fa: F[A])(f: A => G[B])(implicit G: Apply[G]): G[Unit] = {
val f1 = f.andThen(G.void)
reduceRightTo(fa)(f1)((x, y) => G.map2Eval(f1(x), y)((_, b) => b)).value
}
def nonEmptyTraverse_[G[_], A](fa: F[A])(f: A => G[Unit])(implicit G: Apply[G]): G[Unit] =
reduceMapA(fa)(f)

/**
* Sequence `F[G[A]]` using `Apply[G]`.
Expand All @@ -225,7 +223,7 @@ trait Reducible[F[_]] extends Foldable[F] { self =>
* an [[Apply]] instance for `G` instead of [[Applicative]]. See the
* [[nonEmptyTraverse_]] documentation for a description of the differences.
*/
def nonEmptySequence_[G[_], A](fga: F[G[A]])(implicit G: Apply[G]): G[Unit] =
def nonEmptySequence_[G[_]](fga: F[G[Unit]])(implicit G: Apply[G]): G[Unit] =
nonEmptyTraverse_(fga)(identity)

def toNonEmptyList[A](fa: F[A]): NonEmptyList[A] =
Expand Down Expand Up @@ -399,10 +397,10 @@ object Reducible {
typeClassInstance.reduceMapM[G, A, B](self)(f)(G, B)
def reduceRightTo[B](f: A => B)(g: (A, Eval[B]) => Eval[B]): Eval[B] =
typeClassInstance.reduceRightTo[A, B](self)(f)(g)
def nonEmptyTraverse_[G[_], B](f: A => G[B])(implicit G: Apply[G]): G[Unit] =
typeClassInstance.nonEmptyTraverse_[G, A, B](self)(f)(G)
def nonEmptySequence_[G[_], B](implicit ev$1: A <:< G[B], G: Apply[G]): G[Unit] =
typeClassInstance.nonEmptySequence_[G, B](self.asInstanceOf[F[G[B]]])(G)
def nonEmptyTraverse_[G[_]](f: A => G[Unit])(implicit G: Apply[G]): G[Unit] =
typeClassInstance.nonEmptyTraverse_[G, A](self)(f)(G)
def nonEmptySequence_[G[_]](implicit ev$1: A <:< G[Unit], G: Apply[G]): G[Unit] =
typeClassInstance.nonEmptySequence_[G](self.asInstanceOf[F[G[Unit]]])(G)
def toNonEmptyList: NonEmptyList[A] = typeClassInstance.toNonEmptyList[A](self)
def minimum(implicit A: Order[A]): A = typeClassInstance.minimum[A](self)(A)
def maximum(implicit A: Order[A]): A = typeClassInstance.maximum[A](self)(A)
Expand Down
7 changes: 2 additions & 5 deletions core/src/main/scala/cats/instances/list.scala
Expand Up @@ -129,7 +129,7 @@ trait ListInstances extends cats.kernel.instances.ListInstances {
/**
* This avoids making a very deep stack by building a tree instead
*/
override def traverse_[G[_], A, B](fa: List[A])(f: A => G[B])(implicit G: Applicative[G]): G[Unit] = {
override def traverse_[G[_], A](fa: List[A])(f: A => G[Unit])(implicit G: Applicative[G]): G[Unit] = {
// the cost of this is O(size log size)
// c(n) = n + 2 * c(n/2) = n + 2(n/2 log (n/2)) = n + n (logn - 1) = n log n
// invariant: size >= 1
Expand All @@ -155,10 +155,7 @@ trait ListInstances extends cats.kernel.instances.ListInstances {
// failed. We do not use laziness to avoid
// traversing fa, which we will do fully
// in all cases.
Eval.always {
val gb = f(a)
G.void(gb)
}
Eval.always { f(a) }
jpassaro marked this conversation as resolved.
Show resolved Hide resolved
}

val len = fa.length
Expand Down
7 changes: 2 additions & 5 deletions core/src/main/scala/cats/instances/vector.scala
Expand Up @@ -137,7 +137,7 @@ trait VectorInstances extends cats.kernel.instances.VectorInstances {
/**
* This avoids making a very deep stack by building a tree instead
*/
override def traverse_[G[_], A, B](fa: Vector[A])(f: A => G[B])(implicit G: Applicative[G]): G[Unit] = {
override def traverse_[G[_], A](fa: Vector[A])(f: A => G[Unit])(implicit G: Applicative[G]): G[Unit] = {
// the cost of this is O(size)
// c(n) = 1 + 2 * c(n/2)
// invariant: size >= 1
Expand All @@ -161,10 +161,7 @@ trait VectorInstances extends cats.kernel.instances.VectorInstances {
// failed. We do not use laziness to avoid
// traversing fa, which we will do fully
// in all cases.
Eval.always {
val gb = f(a)
G.void(gb)
}
Eval.always { f(a) }
jpassaro marked this conversation as resolved.
Show resolved Hide resolved
}

val len = fa.length
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/scala/cats/syntax/foldable.scala
Expand Up @@ -47,7 +47,8 @@ private[syntax] trait FoldableSyntaxBinCompat1 {
}

final class NestedFoldableOps[F[_], G[_], A](private val fga: F[G[A]]) extends AnyVal {
def sequence_(implicit F: Foldable[F], G: Applicative[G]): G[Unit] = F.sequence_(fga)
def sequence_(implicit F: Foldable[F], G: Applicative[G], ev: A <:< Unit): G[Unit] =
F.sequence_(fga.asInstanceOf[F[G[Unit]]])
Comment on lines +50 to +51
Copy link
Author

Choose a reason for hiding this comment

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

this one is also causing binary compatibility issues, and in this case I don't see how this function can still exist in its current form (i.e. without said evidence) -- I guess it would have to move or something and that would for sure break binary compatibility? not sure if there is a fix here.

Copy link
Member

Choose a reason for hiding this comment

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

Right, we cannot change the signature of this method. What we can do is add a new ops class:

final class NestedUnitFoldableOps[F[_], G[_]](private val fgu: F[G[Unit]])

Copy link
Author

@jpassaro jpassaro Nov 21, 2022

Choose a reason for hiding this comment

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

so this one has to change then to F.traverse_(fga)(G.void(_))?

Copy link
Member

Choose a reason for hiding this comment

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

Seems so.

Copy link
Author

Choose a reason for hiding this comment

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

That still allows people to call .sequence on non-unit structures -- I would suggest deprecating it

Copy link
Member

Choose a reason for hiding this comment

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

though even that seems so knotty I'm not sure it's worthwhile.

honestly, this is my feeling about this PR as a whole. It's obvious at the moment we don't even have existing test coverage to safely make this change.

Copy link
Author

Choose a reason for hiding this comment

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

if we can't change signatures, what about adding new methods with an appropriate signature and deprecating the old ones?

Copy link
Contributor

Choose a reason for hiding this comment

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

I recall I did something similar – i.e. was tuning a method with a very subtle change in its signature while preserving its name: #3997. Not exactly the same though, but there are some similarities apparent.

if we can't change signatures, what about adding new methods with an appropriate signature and deprecating the old ones?

Personally, I don't think it is a good idea, especially because the old methods do not have such bugs that would hinder their usage.

Copy link
Contributor

Choose a reason for hiding this comment

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

@armanbilge

It's obvious at the moment we don't even have existing test coverage to safely make this change.

Do you mean, there's no tests for traverse_ nor sequence_ or do you mean some specific test scenarios?

Copy link
Member

Choose a reason for hiding this comment

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


/**
* @see [[Foldable.foldK]].
Expand Down
6 changes: 3 additions & 3 deletions core/src/main/scala/cats/syntax/parallel.scala
Expand Up @@ -111,7 +111,7 @@ trait ParallelTraverseSyntax {
implicit final def catsSyntaxParallelTraverse_[T[_]: Foldable, A](ta: T[A]): ParallelTraversable_Ops[T, A] =
new ParallelTraversable_Ops(ta)

implicit final def catsSyntaxParallelSequence_[T[_]: Foldable, M[_], A](tma: T[M[A]]): ParallelSequence_Ops[T, M, A] =
implicit final def catsSyntaxParallelSequence_[T[_]: Foldable, M[_]](tma: T[M[Unit]]): ParallelSequence_Ops[T, M] =
new ParallelSequence_Ops(tma)
}

Expand Down Expand Up @@ -189,7 +189,7 @@ final class ParallelSequenceFilterOps[T[_], M[_], A](private val tmoa: T[M[Optio
}

final class ParallelTraversable_Ops[T[_], A](private val ta: T[A]) extends AnyVal {
def parTraverse_[M[_], B](f: A => M[B])(implicit T: Foldable[T], P: Parallel[M]): M[Unit] =
def parTraverse_[M[_]](f: A => M[Unit])(implicit T: Foldable[T], P: Parallel[M]): M[Unit] =
Parallel.parTraverse_(ta)(f)
}

Expand Down Expand Up @@ -217,7 +217,7 @@ final class ParallelSequenceOps1[T[_], M[_], A](private val tma: T[M[A]]) extend
Parallel.parSequence(tma)
}

final class ParallelSequence_Ops[T[_], M[_], A](private val tma: T[M[A]]) extends AnyVal {
final class ParallelSequence_Ops[T[_], M[_]](private val tma: T[M[Unit]]) extends AnyVal {
def parSequence_(implicit T: Foldable[T], P: Parallel[M]): M[Unit] =
Parallel.parSequence_(tma)
}
Expand Down
4 changes: 2 additions & 2 deletions docs/nomenclature.md
Expand Up @@ -115,8 +115,8 @@ Like the previous section, we use the `E` for the error parameter type.
| `F[A] => (A => G[B]) => G[B]` | `foldMapM` | `G: Monad` and `B: Monoid`
| `F[A] => (A => B) => Option[B]` | `collectFirst` | The `A => B` is a `PartialFunction`
| `F[A] => (A => Option[B]) => Option[B]` | `collectFirstSome` |
| `F[A] => (A => G[B]) => G[Unit]` | `traverse_` | `G: Applicative`
| `F[G[A]] => G[Unit]` | `sequence_` | `G: Applicative`
| `F[A] => (A => G[Unit]) => G[Unit]` | `traverse_` | `G: Applicative`
| `F[G[Unit]] => G[Unit]` | `sequence_` | `G: Applicative`
| `F[A] => (A => Either[B, C] => (F[B], F[C])` | `partitionEither` | `G: Applicative`

### Reducible
Expand Down
6 changes: 3 additions & 3 deletions docs/typeclasses/foldable.md
Expand Up @@ -52,10 +52,10 @@ Foldable[List].nonEmpty(List(1,2))
Foldable[Option].toList(Option(1))
Foldable[Option].toList(None)

def parseInt(s: String): Option[Int] = scala.util.Try(Integer.parseInt(s)).toOption
def checkInt(s: String): Option[Unit] = scala.util.Try{ Integer.parseInt(s) ; () }.toOption

Foldable[List].traverse_(List("1", "2"))(parseInt)
Foldable[List].traverse_(List("1", "A"))(parseInt)
Foldable[List].traverse_(List("1", "2"))(checkInt)
Foldable[List].traverse_(List("1", "A"))(checkInt)
Foldable[List].sequence_(List(Option(1), Option(2)))
Foldable[List].sequence_(List(Option(1), None))

Expand Down
2 changes: 1 addition & 1 deletion docs/typeclasses/reducible.md
Expand Up @@ -38,7 +38,7 @@ Reducible[NonEmptyList].reduceLeftTo(NonEmptyList.of(1,2,3,4))(_.toString)((s,i)
Reducible[NonEmptyList].reduceRightTo(NonEmptyList.of(1,2,3,4))(_.toString)((i,s) => Later(s.value + i)).value
Reducible[NonEmptyList].nonEmptyIntercalate(NonEmptyList.of("a", "b", "c"), ", ")

def countChars(s: String) = s.toCharArray.groupBy(identity).view.mapValues(_.length).toMap
def countChars(s: String) = s.toCharArray.groupBy(identity).view.mapValues(x => { x.length ; () }).toMap

Reducible[NonEmptyList].nonEmptyTraverse_(NonEmptyList.of("Hello", "World"))(countChars)
Reducible[NonEmptyVector].nonEmptyTraverse_(NonEmptyVector.of("Hello", ""))(countChars)
Expand Down
4 changes: 2 additions & 2 deletions laws/src/main/scala/cats/laws/ReducibleLaws.scala
Expand Up @@ -53,10 +53,10 @@ trait ReducibleLaws[F[_]] extends FoldableLaws[F] {
def reduceReduceLeftConsistent[B](fa: F[B])(implicit B: Semigroup[B]): IsEq[B] =
fa.reduce <-> fa.reduceLeft(B.combine)

def traverseConsistent[G[_]: Applicative, A, B](fa: F[A], f: A => G[B]): IsEq[G[Unit]] =
def traverseConsistent[G[_]: Applicative, A](fa: F[A], f: A => G[Unit]): IsEq[G[Unit]] =
fa.nonEmptyTraverse_(f) <-> fa.traverse_(f)

def sequenceConsistent[G[_]: Applicative, A](fa: F[G[A]]): IsEq[G[Unit]] =
def sequenceConsistent[G[_]: Applicative](fa: F[G[Unit]]): IsEq[G[Unit]] =
fa.nonEmptySequence_ <-> fa.sequence_

def sizeConsistent[A](fa: F[A]): IsEq[Long] =
Expand Down
Expand Up @@ -44,13 +44,11 @@ trait NonEmptyTraverseTests[F[_]] extends TraverseTests[F] with ReducibleTests[F
ArbYB: Arbitrary[Y[B]],
ArbYC: Arbitrary[Y[C]],
ArbFB: Arbitrary[F[B]],
ArbFM: Arbitrary[F[M]],
ArbXM: Arbitrary[X[M]],
ArbYM: Arbitrary[Y[M]],
ArbFGA: Arbitrary[F[G[A]]],
ArbGU: Arbitrary[G[Unit]],
ArbFGU: Arbitrary[F[G[Unit]]],
ArbFXM: Arbitrary[F[X[M]]],
ArbGB: Arbitrary[G[B]],
ArbGM: Arbitrary[G[M]],
Copy link
Member

Choose a reason for hiding this comment

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

These changes will also cause compatibility problems.

Copy link
Author

Choose a reason for hiding this comment

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

I assumed the correct answer here would be to leave the prior implicit params alone and just add mine at the end of arglist. the mima check is still complaining in 2.12 and I'm not sure how to resolve it

Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately we cannot change these signatures at all: no changes, no additions, no removals. It's not specific to Scala 2.12, that one probably just happened to fail first.

See #4324 (comment) for a possible strategy.

CogenA: Cogen[A],
CogenB: Cogen[B],
CogenC: Cogen[C],
Expand Down
8 changes: 4 additions & 4 deletions laws/src/main/scala/cats/laws/discipline/ReducibleTests.scala
Expand Up @@ -35,8 +35,8 @@ trait ReducibleTests[F[_]] extends FoldableTests[F] {
def reducible[G[_]: Applicative, A: Arbitrary, B: Arbitrary](implicit
ArbFA: Arbitrary[F[A]],
ArbFB: Arbitrary[F[B]],
ArbFGA: Arbitrary[F[G[A]]],
ArbGB: Arbitrary[G[B]],
ArbFGU: Arbitrary[F[G[Unit]]],
ArbGU: Arbitrary[G[Unit]],
Copy link
Member

Choose a reason for hiding this comment

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

Same here.

CogenA: Cogen[A],
CogenB: Cogen[B],
EqG: Eq[G[Unit]],
Expand All @@ -58,8 +58,8 @@ trait ReducibleTests[F[_]] extends FoldableTests[F] {
forAll(laws.reduceRightConsistentWithReduceRightOption[A] _),
"reduce consistent with reduceLeft" ->
forAll(laws.reduceReduceLeftConsistent[B] _),
"nonEmptyTraverse_ consistent with traverse_" -> forAll(laws.traverseConsistent[G, A, B] _),
"nonEmptySequence_ consistent with sequence_" -> forAll(laws.sequenceConsistent[G, A] _),
"nonEmptyTraverse_ consistent with traverse_" -> forAll(laws.traverseConsistent[G, A] _),
"nonEmptySequence_ consistent with sequence_" -> forAll(laws.sequenceConsistent[G] _),
"size consistent with reduceMap" -> forAll(laws.sizeConsistent[A] _)
)
}
Expand Down
Expand Up @@ -179,7 +179,7 @@ trait ScalaVersionSpecificRegressionSuite { self: RegressionSuite =>
// shouldn't have ever evaluated validate(8)
checkAndResetCount(3)

assert(LazyList(1, 2, 6, 8).traverse_(validate) === (Either.left("6 is greater than 5")))
assert(LazyList(1, 2, 6, 8).traverse_(validate(_).void) === (Either.left("6 is greater than 5")))
checkAndResetCount(3)
}
}
Expand Down
18 changes: 9 additions & 9 deletions tests/shared/src/test/scala/cats/tests/ParallelSuite.scala
Expand Up @@ -79,23 +79,23 @@ class ParallelSuite
}

test("ParTraverse_ identity should be equivalent to parSequence_") {
forAll { (es: SortedSet[Either[String, Int]]) =>
assert(Parallel.parTraverse_(es)(identity) === (Parallel.parSequence_[SortedSet, Either[String, *], Int](es)))
forAll { (es: SortedSet[Either[String, Unit]]) =>
assert(Parallel.parTraverse_(es)(identity) === (Parallel.parSequence_[SortedSet, Either[String, *]](es)))
}
}

test("ParTraverse_ syntax should be equivalent to Parallel.parTraverse_") {
forAll { (es: SortedSet[Either[String, Int]]) =>
forAll { (es: SortedSet[Either[String, Unit]]) =>
assert(
Parallel.parTraverse_[SortedSet, Either[String, *], Either[String, Int], Int](es)(identity) === (es
Parallel.parTraverse_[SortedSet, Either[String, *], Either[String, Unit]](es)(identity) === (es
.parTraverse_(identity))
)
}
}

test("ParSequence_ syntax should be equivalent to Parallel.parSequence_") {
forAll { (es: SortedSet[Either[String, Int]]) =>
assert(Parallel.parSequence_[SortedSet, Either[String, *], Int](es) === (es.parSequence_))
forAll { (es: SortedSet[Either[String, Unit]]) =>
assert(Parallel.parSequence_[SortedSet, Either[String, *]](es) === (es.parSequence_))
}
}

Expand All @@ -106,7 +106,7 @@ class ParallelSuite
}

test("ParNonEmptyTraverse_ identity should be equivalent to parNonEmptySequence_") {
forAll { (es: NonEmptyList[Either[String, Int]]) =>
forAll { (es: NonEmptyList[Either[String, Unit]]) =>
assert(Parallel.parNonEmptyTraverse_(es)(identity) === (Parallel.parNonEmptySequence_(es)))
}
}
Expand Down Expand Up @@ -311,8 +311,8 @@ class ParallelSuite
}

test("ParReplicateA_ should be equivalent to fill parSequence_") {
forAll(Gen.choose(1, 20), Arbitrary.arbitrary[Either[String, String]]) {
(repetitions: Int, e: Either[String, String]) =>
forAll(Gen.choose(1, 20), Arbitrary.arbitrary[Either[String, Unit]]) {
(repetitions: Int, e: Either[String, Unit]) =>
assert(Parallel.parReplicateA_(repetitions, e) === Parallel.parSequence_(List.fill(repetitions)(e)))
}
}
Expand Down
Expand Up @@ -238,7 +238,7 @@ abstract class ReducibleSuite[F[_]: Reducible](name: String)(implicit
val notAllEven = fromValues(2, 4, 6, 9, 10, 12, 14)
val out = mutable.ListBuffer[Int]()

notAllEven.nonEmptyTraverse_ { a => out += a; if (a % 2 == 0) Some(a) else None }
notAllEven.nonEmptyTraverse_ { a => out += a; if (a % 2 == 0) Some(()) else None }

assert(out.toList === List(2, 4, 6, 9))
}
Expand Down