typelevel / typelevel/cats

Cats Order needs an `orElseBy` combinator

Open
#4,621 16 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Scala
Stars
5.5k
Forks
1.2k
Avg merge
2d 10h
Merged PRs (30d)
5

Description

Leaving aside the question of whether cats.Order existing (on top of scala.math.Ordering on top of java.util.Comparator) was wise decision 🙄 , at least it ought to be greaterThanOrEqual to its predecessor.

But alas, it lacks something valuable that Ordering has; a convenient syntax to construct a n-level hierarchical ordering for a record, by delegating to the orderings of several record fields. For example, if we have a case class Person(name: String, age: Int) we might wish to order by age but if ages are equal, use name as a discriminator. This is super common IME. Eg SQL has built in syntax for it order by age, name.

There are at least two attempts at addressing this in cats.Order, but alas both are less convenient than the (pre-existing) combinator in scala.math.Ordering:

def orElseBy[S](f: T => S)(implicit ord: Ordering[S]): Ordering[T]

Hence we find code like this in the wild:

      given Order[Single] = Order.fromOrdering(
      Ordering
        .by[Single, VPNTier](_.tier)
        .orElseBy(it => (it.lang1, it.lang2))
        .orElseBy(_.num)
        .orElseBy(_.postfix)
    )

The attempts in cats.Order are:

  • code. Order.whenEqual[Person](Order.by(_.age), Order.by(_.name)). Well it's already looking slightly awkward, but what if we wanted to add a 3rd discriminator? We'd need to nest it inside the second arg with another call to Order.whenEqual. Confusing right nesting.
  • code. The whenEqual Monoid is beautiful and tantalizes us with the promise of val o: Order[Person] = Order.by(_.age) |+| Order.by(_.name). But I could not get the types to infer properly even on Scala 3.5 RC1.

If it ain't broke, don't fix it. orElseBy does the job well and should be brought into Cats.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in cats/kernel/src/main/scala/cats/kernel/Order.scala, around the existing whenEqual methods, and compare scala.math.Ordering.orElseBy. Check how a chained API handles a third discriminator and Scala 3 type inference. Done means Cats offers an orElseBy-style combinator that supports the Person age/name example and preserves the intended ordering behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
scala
Domain
developer-experience
Issue type
Feature
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.