skip to content

How do you build a result ordering like 'best match first, then earliest application' from small comparators rather than one comparison function?

level: middleimportance: must knowfreq 58%

answer

  1. three verdicts, not two
  2. equal is what delegates onward
  3. one key and one direction per stage
  4. reversing the chain reverses tiebreakers too
  5. a unique final key makes it deterministic

basics

~20 s

Give each comparator one key and one direction, then chain them: the next comparator is consulted only when the previous one reports a tie. Each returns a three-way verdict - before, equal, after - and 'equal' is the signal that hands control to the tiebreaker.

solid answer

~40 s

A comparator takes two candidates and answers with one of three verdicts: first comes before second, they are equivalent, or first comes after. Chaining is the same shape as conjunction for predicates: the combined comparator asks the first, returns its verdict unless it is 'equal', and otherwise delegates to the next. So 'best match first, then earliest application' is `chain(byScoreDescending, byAppliedAtAscending)`. Each stage carries its own key and its own direction, which is what lets you reverse one stage without touching the others. Reversing the whole chain reverses every stage, tiebreakers included - usually not what a UI's 'sort descending' toggle means. If the last stage can still report a tie, equal candidates come out in whatever order the sort left them, so a unique final key gives you a deterministic listing.

code

pseudocode · 8 lines
pseudocode
function chain(first, second)
  return function(a, b)
    verdict = first(a, b)
    if verdict is not EQUIVALENT then return verdict
    return second(a, b)

ordering = chain(reverse(byScore),
            chain(byAppliedAt, byIdentifier))

go deeper

for a junior

Know that a comparator answers with three outcomes, and that chaining means the second one is consulted only when the first calls the pair equivalent.

for a middle

Explain the delegation rule precisely and keep direction attached to each stage. Be able to say what reversing the whole chain does to the tiebreakers.

for a senior

Bring the consequence: a listing with no unique final stage reshuffling equal rows between page loads, and a comparison stage that violated transitivity and broke the sort itself.

for a principal

Set the house rule - every list ordering ends on a unique key, direction toggles flip only the leading stage - and decide where ordering is expressed in the query rather than assembled in memory.

## A comparator is a three-way answer Where a predicate answers one question about one value, a **comparator** answers a question about a **pair**: given two candidates, does the first belong before the second, after it, or are they equivalent for this ordering? The three-way verdict is the whole mechanism - it is what makes composition possible, because "equivalent" is a distinct outcome the next stage can act on. A two-valued "is first smaller" answer cannot distinguish *equal* from *greater*, so nothing downstream knows when to take over. ## Chaining: the conjunction of orderings The combinator has the same shape as the conjunction of predicates, with "equal" playing the role that "true" plays there: 1. Ask the first comparator about the pair. 2. If its verdict is anything other than *equivalent*, that verdict is the answer - stop. 3. Otherwise, ask the next comparator, and repeat until one decides or the chain runs out. So a result list ordered "best match first, then earliest application, then by identifier" is three tiny comparators chained, each of which is independently readable and independently testable. Compare that with a single comparison function containing nested conditionals: the same logic, but you cannot reuse "earliest application" anywhere else, and every new tiebreaker edits the same block. ## Direction belongs to the stage, not the chain This is where interviewers push, and where answers most often come out backwards. | What you want | How you build it | |---|---| | Score descending, then application ascending | reverse the score stage only, chain it before the ascending stage | | Every key reversed | reverse the whole chain - each stage flips, tiebreakers included | | A stable "sort by" toggle in a UI | flip only the leading stage, leave the tiebreakers fixed | Reversing the assembled chain is not the same as reversing its first stage. It flips every stage, so "earliest application" silently becomes "latest application" too. A user toggling a column header almost never means that, which makes it a real and easily shipped defect. ## What the pieces have to promise For a chained ordering to be usable by a sort at all, each stage must behave consistently: - **Consistent with itself** - asking the same pair twice gives the same verdict, so a stage must not depend on mutable state it can observe changing mid-sort. - **Antisymmetric** - if it puts A before B, it must put B after A. A stage built from a subtraction of two computed numbers is a classic place this quietly fails at extremes. - **Transitive** - if A before B and B before C, then A before C. A hand-rolled "looks about right" comparison that violates this can make a sort produce nonsense or refuse to finish. - **Total, or explicitly tie-breaking** - if the final stage can report *equivalent*, the pairs it ties are left in whatever relative order the sorting algorithm produces, which may differ between runs or between machines. That last point is the practical one: **a deterministic listing needs a final stage on a unique key**, typically the record's identifier. Without it, two equally-scored applications submitted in the same second can swap places between page loads, which reads to users as a paging bug - the same candidate appearing twice, or not at all, across consecutive pages. ## Where the analogy to predicate combinators stops Both families are built the same way - small named pieces, a combinator that assembles them, one value at the end - and both put the cheap, decisive test first for the same reason: a stage that rarely reports a tie means later stages are rarely consulted. But the two differ in one important respect. Predicate combinators come in several flavours, because a candidate can be required to satisfy every part or merely one of them. Comparators have no meaningful "or": an ordering by score *or* by date is not an ordering at all, because it would not give a consistent verdict for the same pair. The only combinators a comparator supports are chaining, reversing, and building one from a key extractor - which is why the vocabulary here is smaller and more regular than for predicates.

  • What breaks if the final stage in the chain can still report a tie?
    Nothing crashes, but the listing stops being deterministic: tied records come out in whatever relative order the sort happened to leave them, which can differ between runs, data sets or implementations. Users see it as rows shuffling between page loads, or a record appearing on two consecutive pages. Ending the chain on a unique key removes it.
  • A user flips the sort direction in the UI. What exactly do you reverse?
    Usually the leading stage only. Reversing the assembled chain flips every stage, so the tiebreakers invert as well - 'earliest application' becomes 'latest' - which is rarely what the toggle promised. Keeping direction as a property of each stage is what makes the narrow reversal expressible at all.
  • Why can you chain comparators but not build an 'or' of two orderings?
    Because an ordering must give one consistent verdict for a pair. Combining two orderings disjunctively could put A before B by one key and after it by the other, and no sort can honour both. Chaining avoids this by consulting the second stage only where the first is genuinely indifferent.

saying these in an interview costs you the question

  • Has the comparator return only a two-valued answer
  • Says reversing the chain flips just the first key
  • Chains a further stage after a decisive verdict
  • Builds an ordering from a concatenation of keys as text
  • Leaves the chain ending on a non-unique key and expects stable output
  • Believes orderings can be combined disjunctively like predicates