From 901eb3d67d2cf6dc8e90990abda0a3e5f95fd20d Mon Sep 17 00:00:00 2001 From: Jorge Adriano Date: Sat, 20 Jun 2026 00:00:05 +0200 Subject: [PATCH 1/3] Add unorderedReduceOption to UnorderedFoldable --- core/src/main/scala/cats/UnorderedFoldable.scala | 7 ++++++- .../src/main/scala/cats/syntax/unorderedFoldable.scala | 5 +++++ .../main/scala/cats/laws/UnorderedFoldableLaws.scala | 3 +++ .../cats/laws/discipline/UnorderedFoldableTests.scala | 6 +++++- .../test/scala/cats/tests/UnorderedFoldableSuite.scala | 10 ++++++++++ 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/cats/UnorderedFoldable.scala b/core/src/main/scala/cats/UnorderedFoldable.scala index 09ee27b4c6..efe551bb38 100644 --- a/core/src/main/scala/cats/UnorderedFoldable.scala +++ b/core/src/main/scala/cats/UnorderedFoldable.scala @@ -21,7 +21,7 @@ package cats -import cats.kernel.CommutativeMonoid +import cats.kernel.{CommutativeMonoid, CommutativeSemigroup} import scala.collection.immutable.{Queue, Seq, SortedMap, SortedSet} import scala.util.Try @@ -35,6 +35,11 @@ trait UnorderedFoldable[F[_]] extends Serializable { def unorderedFold[A: CommutativeMonoid](fa: F[A]): A = unorderedFoldMap(fa)(identity) + def unorderedReduceOption[A](fa: F[A])(implicit A: CommutativeSemigroup[A]): Option[A] = + unorderedFoldMap(fa)(a => Some(a): Option[A])( + cats.kernel.instances.option.catsKernelStdCommutativeMonoidForOption + ) + /** * Fold in a [[CommutativeApplicative]] context by mapping the `A` values to `G[B]`. combining * the `B` values using the given `CommutativeMonoid[B]` instance. diff --git a/core/src/main/scala/cats/syntax/unorderedFoldable.scala b/core/src/main/scala/cats/syntax/unorderedFoldable.scala index 2a793c2552..1313829840 100644 --- a/core/src/main/scala/cats/syntax/unorderedFoldable.scala +++ b/core/src/main/scala/cats/syntax/unorderedFoldable.scala @@ -22,6 +22,8 @@ package cats package syntax +import cats.kernel.CommutativeSemigroup + trait UnorderedFoldableSyntax extends UnorderedFoldable.ToUnorderedFoldableOps { implicit final def catsSyntaxUnorderedFoldableOps[F[_]: UnorderedFoldable, A](fa: F[A]): UnorderedFoldableOps[F, A] = new UnorderedFoldableOps[F, A](fa) @@ -66,4 +68,7 @@ final class UnorderedFoldableOps[F[_], A](private val fa: F[A]) extends AnyVal { */ def count(p: A => Boolean)(implicit F: UnorderedFoldable[F]): Long = F.count(fa)(p) + + def unorderedReduceOption(implicit A: CommutativeSemigroup[A], F: UnorderedFoldable[F]): Option[A] = + F.unorderedReduceOption(fa) } diff --git a/laws/src/main/scala/cats/laws/UnorderedFoldableLaws.scala b/laws/src/main/scala/cats/laws/UnorderedFoldableLaws.scala index e70df54e0c..5bc1320b32 100644 --- a/laws/src/main/scala/cats/laws/UnorderedFoldableLaws.scala +++ b/laws/src/main/scala/cats/laws/UnorderedFoldableLaws.scala @@ -33,6 +33,9 @@ trait UnorderedFoldableLaws[F[_]] { def unorderedFoldMapAIdentity[A, B: CommutativeMonoid](fa: F[A], f: A => B): IsEq[B] = F.unorderedFoldMapA[Id, A, B](fa)(f) <-> F.unorderedFoldMap(fa)(f) + def unorderedReduceOptionConsistentWithUnorderedFold[A: CommutativeMonoid](fa: F[A]): IsEq[Option[A]] = + F.unorderedReduceOption(fa) <-> (if (F.isEmpty(fa)) None else Some(F.unorderedFold(fa))) + def forallConsistentWithExists[A](fa: F[A], p: A => Boolean): Boolean = if (F.forall(fa)(p)) { val negationExists = F.exists(fa)(a => !p(a)) diff --git a/laws/src/main/scala/cats/laws/discipline/UnorderedFoldableTests.scala b/laws/src/main/scala/cats/laws/discipline/UnorderedFoldableTests.scala index 2d6ee98da9..d1d47fde86 100644 --- a/laws/src/main/scala/cats/laws/discipline/UnorderedFoldableTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/UnorderedFoldableTests.scala @@ -28,6 +28,7 @@ import Prop.* import org.typelevel.discipline.Laws import cats.kernel.CommutativeMonoid import cats.instances.boolean.* +import cats.instances.option.* trait UnorderedFoldableTests[F[_]] extends Laws { def laws: UnorderedFoldableLaws[F] @@ -53,7 +54,10 @@ trait UnorderedFoldableTests[F[_]] extends Laws { "forall is lazy" -> forAll(laws.forallLazy[A] _), "contains consistent with exists" -> forAll(laws.containsConsistentWithExists[A] _), "contains consistent with forall" -> forAll(laws.containsConsistentWithForall[A] _), - "contains all elements from itself" -> forAll(laws.containsAllElementsFromItself[A] _) + "contains all elements from itself" -> forAll(laws.containsAllElementsFromItself[A] _), + "unorderedReduceOption consistent with unorderedFold" -> forAll( + laws.unorderedReduceOptionConsistentWithUnorderedFold[A] _ + ) ) } diff --git a/tests/shared/src/test/scala/cats/tests/UnorderedFoldableSuite.scala b/tests/shared/src/test/scala/cats/tests/UnorderedFoldableSuite.scala index 135a4c1e51..4579875a50 100644 --- a/tests/shared/src/test/scala/cats/tests/UnorderedFoldableSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/UnorderedFoldableSuite.scala @@ -76,6 +76,16 @@ sealed abstract class UnorderedFoldableSuite[F[_]](name: String)(implicit } } + test(s"UnorderedFoldable[$name].unorderedReduceOption") { + forAll { (fa: F[Int]) => + implicit val F: UnorderedFoldable[F] = instance + val expected = + if (instance.isEmpty(fa)) None + else Some(instance.unorderedFold(fa)) + assert(fa.unorderedReduceOption === expected) + } + } + checkAll("F[Int]", UnorderedFoldableTests[F](using instance).unorderedFoldable[Int, Int]) } From 4eadd23e2eaaa3b9c6188a8f5bb3e5a1f1c29612 Mon Sep 17 00:00:00 2001 From: Jorge Adriano Date: Sat, 20 Jun 2026 15:18:14 +0200 Subject: [PATCH 2/3] Improve unorderedReduceOption implementation and add docs --- core/src/main/scala/cats/UnorderedFoldable.scala | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/cats/UnorderedFoldable.scala b/core/src/main/scala/cats/UnorderedFoldable.scala index efe551bb38..832cfce288 100644 --- a/core/src/main/scala/cats/UnorderedFoldable.scala +++ b/core/src/main/scala/cats/UnorderedFoldable.scala @@ -35,10 +35,15 @@ trait UnorderedFoldable[F[_]] extends Serializable { def unorderedFold[A: CommutativeMonoid](fa: F[A]): A = unorderedFoldMap(fa)(identity) - def unorderedReduceOption[A](fa: F[A])(implicit A: CommutativeSemigroup[A]): Option[A] = - unorderedFoldMap(fa)(a => Some(a): Option[A])( - cats.kernel.instances.option.catsKernelStdCommutativeMonoidForOption - ) + /** + * Reduce this unordered structure by combining its elements with a [[CommutativeSemigroup]] instance. + * + * If there are no elements, the result is `None`. + */ + def unorderedReduceOption[A: CommutativeSemigroup](fa: F[A]): Option[A] = { + val reducer = CommutativeMonoid[Option[A]] + unorderedFoldMap(fa)(a => Some(a): Option[A])(using reducer) + } /** * Fold in a [[CommutativeApplicative]] context by mapping the `A` values to `G[B]`. combining From c9568710a26b0931b85fbe18f676f123374ddaa1 Mon Sep 17 00:00:00 2001 From: Jorge Adriano Date: Sat, 20 Jun 2026 15:21:54 +0200 Subject: [PATCH 3/3] Drop redundant test --- .../test/scala/cats/tests/UnorderedFoldableSuite.scala | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/shared/src/test/scala/cats/tests/UnorderedFoldableSuite.scala b/tests/shared/src/test/scala/cats/tests/UnorderedFoldableSuite.scala index 4579875a50..135a4c1e51 100644 --- a/tests/shared/src/test/scala/cats/tests/UnorderedFoldableSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/UnorderedFoldableSuite.scala @@ -76,16 +76,6 @@ sealed abstract class UnorderedFoldableSuite[F[_]](name: String)(implicit } } - test(s"UnorderedFoldable[$name].unorderedReduceOption") { - forAll { (fa: F[Int]) => - implicit val F: UnorderedFoldable[F] = instance - val expected = - if (instance.isEmpty(fa)) None - else Some(instance.unorderedFold(fa)) - assert(fa.unorderedReduceOption === expected) - } - } - checkAll("F[Int]", UnorderedFoldableTests[F](using instance).unorderedFoldable[Int, Int]) }