Skip to content

Add minimumOption and maximumOption to UnorderedFoldable - #4911

Open
jorgeadriano wants to merge 4 commits into
typelevel:mainfrom
jorgeadriano:unordered-minmax
Open

jorgeadriano wants to merge 4 commits into
typelevel:mainfrom
jorgeadriano:unordered-minmax

Conversation

@jorgeadriano

@jorgeadriano jorgeadriano commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #4876. Closes #2715 and #3309.

Summary

minimumOption and maximumOption currently require Foldable, although finding an extremum does not require an ordered traversal. This PR adds both methods to UnorderedFoldable, making them available for unordered structures such as Set and Map, and for generic code constrained only by UnorderedFoldable.

The new defaults are implemented in terms of unorderedReduceOption. They return None for empty structures; if several elements compare as equal according to Order[A], any of them may be returned.

minimumByOption and maximumByOption remain on Foldable: when distinct A values map to equal ordered keys, the key order does not provide an equality on A that would make choosing one representative an unordered operation.

Existing Foldable.minimumOption / maximumOption implementations are retained as explicit overrides, so ordered structures keep their existing reduction path. No new abstract method is added to UnorderedFoldable instances.

Syntax and compatibility

The existing extrema declarations on both Foldable and Foldable.Ops are retained to preserve their binary entry points. The syntax hierarchy also becomes:

trait Foldable.Ops[F[_], A]
  extends UnorderedFoldable.Ops[F, A]

trait Foldable.ToFoldableOps
  extends UnorderedFoldable.ToUnorderedFoldableOps

These relationships solve two separate source-compilation issues in this design:

  • Without the Ops inheritance, Foldable.AllOps inherits competing concrete extrema definitions from the ordered and unordered syntax carriers.
  • Once the carriers are related, leaving the converter-owner traits unrelated makes generic syntax ambiguous between toFoldableOps and toUnorderedFoldableOps. Relating the owners gives the Foldable conversion the required specificity.

This explains why both relationships are present in the chosen design. One small source-surface consequence is that the deprecated Foldable.nonInheritedOps also inherits unordered syntax despite its historical name.

Compiler evidence / reproduction

Each experiment starts independently from a clean checkout of the PR and changes one declaration in Foldable.scala.

A. Foldable.Ops

Temporarily change:

trait Ops[F[_], A] extends UnorderedFoldable.Ops[F, A]

to:

trait Ops[F[_], A] extends Serializable

Run:

sbt '++2.13.18' 'coreJVM/compile'

Compilation fails because the retained extrema methods no longer override the unordered carrier, while Foldable.AllOps inherits competing concrete extrema definitions. The diagnostics include minimumOption overrides nothing, maximumOption overrides nothing, and conflicting members in AllOps.

Restoring the original parent and rerunning the same command passes. A reduced carrier-only model reproduces the concrete-member conflict on Scala 2.12.21, 2.13.18, and 3.3.8.

B. ToFoldableOps

Keep the working Ops hierarchy and temporarily change:

trait ToFoldableOps
  extends UnorderedFoldable.ToUnorderedFoldableOps

to:

trait ToFoldableOps extends Serializable

Run:

sbt '++2.13.18'   'coreJVM/clean'   'coreJVM/compile'   'testsJVM/Test/compile'

Core compilation passes, but generic syntax in the laws/tests fails with ambiguous toFoldableOps / toUnorderedFoldableOps conversions. A minimal example is:

import cats.Foldable
import cats.syntax.foldable._

object SizeRepro {
  def size[F[_]: Foldable, A](fa: F[A]): Long = fa.size
}

The same ambiguity was reproduced for size, exists, minimumOption, and maximumOption on Scala 2.12.21, 2.13.18, and 3.3.8. Restoring the owner inheritance makes the same sources compile; typed trees select toFoldableOps.

These experiments cover source compilation only; binary compatibility is checked separately.

Tests and validation

Coverage includes:

  • correctness on empty and non-empty structures, including that returned values are the actual minima/maxima according to Order;
  • behavior on unordered instances such as Set and Map, including tied values where any Order-equal minimum/maximum may be returned;
  • syntax through unorderedFoldable, foldable, combined imports, and cats.syntax.all, including stronger Traverse and Reducible contexts;
  • preservation of the existing Foldable extrema implementations for ordered types;
  • binary compatibility with old compiled direct callers, syntax callers, Foldable.Ops / AllOps callers, and old UnorderedFoldable implementations using the new default methods.

The focused syntax suite passes on Scala 2.12.21, 2.13.18, and 3.3.8. MiMa and the old-client compatibility fixtures pass, as do the broader JVM / Scala.js checks and the affected Scala Native checks.

Copilot AI balanced review requested due to automatic review settings October 5, 2026 10:14

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.

Copilot review overview

🔵 Needs a closer look

It alters the core Foldable/UnorderedFoldable type-class and syntax hierarchy with binary-compatibility implications, which warrants final human review.

Review effort: Balanced
Findings: None

What changed in this PR

This PR moves minimumOption and maximumOption down from Foldable to UnorderedFoldable, since finding an extremum is a commutative/unordered operation. The new defaults are implemented in terms of unorderedReduceOption (added in #4876), making extrema available for unordered structures like Set and Map and for code generically constrained to UnorderedFoldable. Foldable keeps its existing reduceLeftOption-based implementations as explicit overrides, and the syntax carrier/converter traits are re-parented so the Foldable conversion remains unambiguous. minimumByOption/maximumByOption intentionally stay on Foldable.

Changes:

  • Add minimumOption/maximumOption defaults and Ops syntax to UnorderedFoldable, implemented via unorderedReduceOption; convert the Foldable equivalents to override.
  • Re-parent Foldable.Ops → UnorderedFoldable.Ops and Foldable.ToFoldableOps → UnorderedFoldable.ToUnorderedFoldableOps to avoid conflicting/ambiguous syntax.
  • Add order-consistency laws plus a new unorderedFoldableOrder rule set, and extensive unit, syntax, and binary-compatibility fixtures.
File Description
core/​src/​main/​scala/​cats/​UnorderedFoldable.scala Adds minimumOption/maximumOption defaults (via unorderedReduceOption) and their Ops syntax.
core/​src/​main/​scala/​cats/​Foldable.scala Marks the existing extrema as override; re-parents Ops and ToFoldableOps to the unordered carriers.
laws/​src/​main/​scala/​cats/​laws/​UnorderedFoldableLaws.scala Adds minimumOption/maximumOption consistency-with-Order laws.
laws/​src/​main/​scala/​cats/​laws/​discipline/​UnorderedFoldableTests.scala Adds the unorderedFoldableOrder rule set wiring the new laws.
tests/​.../​UnorderedFoldableSuite.scala Adds a syntax dispatch test and checks the new order rule set for Set/Map.
tests/​.../​UnorderedFoldableSyntaxSuite.scala New suite exercising syntax import combinations, override dispatch, and permutation invariance.
binCompatTest/​.../​FoldableMinimumMaximum.scala New fixtures (old-compiled instances/callers) for forward binary compatibility.
binCompatTest/​.../​FoldableMinimumMaximumTest.scala New tests asserting old callers inherit the new defaults against current Cats.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

This branch has not been deployed

No deployments
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.

minimumOption and maximumOption could be on UnorderedFoldable

2 participants