Skip to content

Add unorderedReduceOption to UnorderedFoldable#4876

Open
jorgeadriano wants to merge 1 commit into
typelevel:mainfrom
jorgeadriano:feature/unordered-reduce-option
Open

Add unorderedReduceOption to UnorderedFoldable#4876
jorgeadriano wants to merge 1 commit into
typelevel:mainfrom
jorgeadriano:feature/unordered-reduce-option

Conversation

@jorgeadriano

Copy link
Copy Markdown

Add unorderedReduceOption to UnorderedFoldable

Motivation

Foldable has ordered reduction methods such as reduceLeftOption and reduceRightOption,
but UnorderedFoldable does not currently have an unordered equivalent for reducing a
possibly-empty structure.

This PR adds unorderedReduceOption, which reduces an unordered structure to Option[A],
returning None for empty structures and Some(combined) for non-empty ones.

The constraint is CommutativeSemigroup[A], rather than Semigroup[A], because
UnorderedFoldable provides no guarantee on element order.

This is related to the discussion in #2715 and extracts the focused unorderedReduceOption
part of #3309. Unlike #3309, this PR intentionally leaves minimumOption, maximumOption,
minimumByOption, and maximumByOption out of scope, since those raise separate API and
binary-compatibility considerations, as also discussed around #3132.

API

def unorderedReduceOption[A](fa: F[A])(implicit A: CommutativeSemigroup[A]): Option[A]

Also available as syntax via cats.syntax.all._:

import cats.syntax.all._

Set(1, 2, 3).unorderedReduceOption   // Some(6)
Set.empty[Int].unorderedReduceOption // None

Implementation

The implementation is defined in terms of unorderedFoldMap, by lifting each A into
Option[A] and folding with the existing CommutativeMonoid[Option[A]] instance derived
from CommutativeSemigroup[A].

Law

The PR adds a law checking consistency with unorderedFold:

def unorderedReduceOptionConsistentWithUnorderedFold[A: CommutativeMonoid](fa: F[A]): IsEq[Option[A]] =
  F.unorderedReduceOption(fa) <-> (if (F.isEmpty(fa)) None else Some(F.unorderedFold(fa)))

The law is wired into the existing UnorderedFoldableTests.unorderedFoldable rule set.
Eq[Option[A]] is resolved internally from the existing Eq[A] instance via
cats.instances.option, so the public signature of unorderedFoldable is unchanged.

Included changes

  • UnorderedFoldable#unorderedReduceOption
  • syntax support via cats.syntax.all._
  • a law checking consistency with unorderedFold
  • discipline test integration
  • a focused test in UnorderedFoldableSuite

Copilot AI review requested due to automatic review settings June 20, 2026 00:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds unorderedReduceOption to UnorderedFoldable to support reducing possibly-empty unordered structures, with accompanying syntax and law/test coverage to ensure consistency with unorderedFold.

Changes:

  • Add UnorderedFoldable#unorderedReduceOption implemented via unorderedFoldMap over Option[A].
  • Add syntax support (fa.unorderedReduceOption) under cats.syntax.all._.
  • Add a new law + discipline wiring, plus a focused suite test.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
core/src/main/scala/cats/UnorderedFoldable.scala Adds the new unorderedReduceOption API on the typeclass.
core/src/main/scala/cats/syntax/unorderedFoldable.scala Exposes unorderedReduceOption as syntax on F[A].
laws/src/main/scala/cats/laws/UnorderedFoldableLaws.scala Adds a law relating unorderedReduceOption to unorderedFold/isEmpty.
laws/src/main/scala/cats/laws/discipline/UnorderedFoldableTests.scala Wires the new law into the existing unorderedFoldable RuleSet.
tests/shared/src/test/scala/cats/tests/UnorderedFoldableSuite.scala Adds a direct property test for unorderedReduceOption.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +38 to +41
def unorderedReduceOption[A](fa: F[A])(implicit A: CommutativeSemigroup[A]): Option[A] =
unorderedFoldMap(fa)(a => Some(a): Option[A])(
cats.kernel.instances.option.catsKernelStdCommutativeMonoidForOption
)
Comment on lines +71 to +73

def unorderedReduceOption(implicit A: CommutativeSemigroup[A], F: UnorderedFoldable[F]): Option[A] =
F.unorderedReduceOption(fa)
@satorg

satorg commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Thank you for the PR! The change makes sense to me. A few rather minor comments from my side are inline below.

Comment on lines +38 to +41
def unorderedReduceOption[A](fa: F[A])(implicit A: CommutativeSemigroup[A]): Option[A] =
unorderedFoldMap(fa)(a => Some(a): Option[A])(
cats.kernel.instances.option.catsKernelStdCommutativeMonoidForOption
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  def unorderedReduceOption[A: CommutativeSemigroup](fa: F[A]): Option[A] = {
    val reducer = CommutativeMonoid[Option[A]]
    unorderedFoldMap(fa)(a => Some(a): Option[A])(using reducer)
  }
  1. implicit A only makes sense if the code needs to refer to A directly. It is not the case here, so I'd suggest using the context bounds syntax, as it seems to be more concise and readable.
  2. Referring to catsKernelStdCommutativeMonoidForOption directly is rather brittle and doesn't seem necessary. I'd suggest to summon the required instance and pass it to unorderedFoldMap.

Comment on lines +79 to +87
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)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this test somehow different from the unorderedReduceOptionConsistentWithUnorderedFold check in UnorderedFoldableLaws? If not, then we can probably drop this one in order to avoid bloating tests with identical checks.

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