skip to content

A pull request's sort.Interface Less returns true for rows equal on every key — what breaks?

level: seniorimportance: should knowfreq 44%

answer

  1. equal means false in both directions
  2. strictly before, not before-or-equal
  3. the sort only ever swaps
  4. stability needs ties it can see
  5. IsSorted can fail right after Sort

basics

~20 s

The ordering stops being consistent: no pair is ever recognised as equal, so the resulting order is arbitrary, sort.Stable can no longer preserve input order, and sort.IsSorted may report false on the sort's own output. Nothing is lost — the sort only swaps.

solid answer

~50 s

`Less(i, j)` must report whether `i` **must** sort before `j`; two elements count as equal exactly when `Less(i, j)` and `Less(j, i)` are both false. A `Less` written with `<=`, or one that returns true on a tie, makes both directions true, so the sort never sees any pair as equal and the documented transitivity requirement is violated. The consequences are ordering consequences, not corruption: `sort.Sort` produces an arbitrary order among tied rows that can differ from `sort.Stable`'s and can change with the Go version; `sort.Stable`'s guarantee is void because it never identifies the ties it is meant to preserve; and `sort.IsSorted` can report false on data the same call just sorted. No row is lost or duplicated — the sort only calls `Swap`, so the output is always a permutation of the input. In review I would ask for `<` plus a unique tie-breaker, and a pair-level test asserting the two directions are never both true.

code

go · 12 lines
go
// WRONG: for equal Scores, Less(i, j) and Less(j, i) are both true
func (r byRank) Less(i, j int) bool {
	return r[i].Score <= r[j].Score
}

// Right: strict on the key, with a unique final tie-breaker
func (r byRank) Less(i, j int) bool {
	if r[i].Score != r[j].Score {
		return r[i].Score < r[j].Score
	}
	return r[i].ID < r[j].ID
}

go deeper

for a junior

Remember the rule of thumb: use strict less-than in Less, never less-than-or-equal, and add a second field to break ties rather than returning true for equal rows.

for a middle

Explain the package's definition of equality — both directions false — and the transitivity requirements, then show why a non-strict comparison violates them and leaves the output order undefined.

for a senior

Demonstrate the diagnosis: assert the pair property in a test, call IsSorted after Sort, diff Sort against Stable on tied input, and know that the failure is a meaningless order rather than lost or duplicated rows.

for a principal

Push the fix upstream of the incident. Decide whether every ordering in the codebase must end on a unique key so output is reproducible across toolchain upgrades, and whether that invariant is enforced by review habit, a shared helper, or a test the ordering types must pass.

## The contract being broken Go's `sort` package documents `Less(i, j int) bool` as reporting whether the element at index `i` **must sort before** the element at index `j`, and it spells out the consequences: - If both `Less(i, j)` and `Less(j, i)` are false, the two elements are **considered equal**. `sort.Sort` may place equals in any order; `sort.Stable` preserves their input order. - `Less` must describe a **transitive** ordering: if `Less(i, j)` and `Less(j, k)` are both true then `Less(i, k)` must be true; and if `Less(i, j)` and `Less(j, k)` are both **false** then `Less(i, k)` must be false too. A method that returns true for two rows equal on every key — the usual cause is `<=` where `<` was meant — makes both directions true for that pair. Equality can never be detected, and the second transitivity rule fails immediately: for two equal rows `x` and `y`, `Less(x, y)` is true rather than false, so any reasoning the algorithm does about runs of equals is built on a lie. ## What actually happens at run time This is the part candidates most often get wrong in both directions. **It does not corrupt the data.** The sort moves elements only through your `Swap` method, and `Swap` exchanges two positions. Whatever sequence of swaps the algorithm chooses, the output is a **permutation** of the input: the same rows, all of them, once each. A colleague reporting that rows "duplicated" or "vanished" between runs is describing something else — a rendering or pagination step downstream that keyed off position, a `Len` that lies about the length, or elements being copied out of the slice mid-sort. **It does not panic or hang.** Every index the package passes to `Less` and `Swap` is inside `[0, Len())`, and the algorithm terminates regardless of what your comparisons say. **It does produce a meaningless order.** Which permutation you get depends on the algorithm's internal decisions, so it can differ between `sort.Sort` and `sort.Stable` on identical input, and it can change when the toolchain's sort implementation changes. That is exactly the failure that surfaces as a report whose tied rows shuffle after a routine toolchain upgrade, with no application change to blame. **It voids stability.** `sort.Stable` preserves the input order *of elements it considers equal*. With a non-strict `Less`, it considers nothing equal, so it has nothing to preserve — the guarantee the caller was relying on silently disappears. ## How to demonstrate it in review Three cheap moves, in increasing order of effort: 1. **Assert the pair property directly.** For any two rows the reviewer believes are ties, `Less(i, j) && Less(j, i)` must be false. One table-driven test over a handful of representative rows catches the whole class: ```go for i := range rows { for j := range rows { if data.Less(i, j) && data.Less(j, i) { t.Fatalf("rows %d and %d each sort before the other", i, j) } } } ``` 2. **Sort, then ask the package.** `sort.IsSorted(data)` immediately after `sort.Sort(data)` should be true. When the ordering is inconsistent it can come back false, which is a striking artefact to put in a PR comment. 3. **Diff the two sorts.** Build one input with deliberate ties, sort a copy with `sort.Sort` and another with `sort.Stable`, and diff the results. With a correct strict `Less`, the two agree except in the relative order of genuine equals — and if the cascade ends in a unique key they agree exactly. With a broken `Less`, they diverge in ways nobody can explain, which is the clearest evidence the ordering itself is at fault rather than the data. An **instrumented wrapper** helps when you want more than a yes/no. Embed the real `sort.Interface` in a struct, count `Less` and `Swap` calls, and record every pair for which both directions came back true. It costs a dozen lines, it drops in without touching the code under review, and it names the exact rows that violate the contract. ## The fix Strict comparisons on every key — `<` or `>`, never `<=` or `>=` — with each key guarded by an equality test, and a final key that is unique per row. Once no two distinct rows compare equal, the ordering is total: `sort.Sort` and `sort.Stable` produce the same output, the rendered report is reproducible run to run and version to version, and the choice between the two sort functions stops mattering.

  • Could a broken Less cause rows to be lost or duplicated?
    No. The sort moves elements only through `Swap`, which exchanges two positions, so whatever order comes out is a permutation of what went in — every row present exactly once. Rows that appear to duplicate or vanish point at something downstream: a renderer or pager keyed on position, a `Len` that reports the wrong count, or elements copied out of the slice while it was being sorted.
  • Why does sort.Stable stop helping when Less is not strict?
    `sort.Stable` preserves the input order of elements it *considers equal*, and equality is defined as both `Less(i, j)` and `Less(j, i)` being false. A non-strict `Less` makes both true instead, so the sort never identifies a single tie and has nothing to keep in place. The guarantee does not fail loudly — it silently has no subjects.
  • How would you prove the bug to the author in the pull request itself?
    Add a test that loops over representative rows and fails whenever `Less(i, j)` and `Less(j, i)` are both true — it names the offending pair. Back it up by calling `sort.IsSorted` right after `sort.Sort` on the same data, which can return false, and by diffing `sort.Sort` against `sort.Stable` over the same tied input.
  • The tied rows in a report shuffled after a toolchain upgrade with no code change. What is your first hypothesis?
    That the ordering was never total: the `Less` leaves genuine ties, or is outright inconsistent, and the previous arrangement was an accident of the sort implementation rather than anything the code specified. Confirm by checking whether tied rows exist at all, then fix it by ending the comparison cascade on a unique key so the output no longer depends on the algorithm.

saying these in an interview costs you the question

  • Says a broken Less makes elements disappear or duplicate
  • Claims the sort package validates the ordering and panics
  • Thinks sort.Stable repairs an inconsistent comparison
  • Believes equal elements are those where Less returns true both ways
  • Blames flaky tied-row order on the data instead of the ordering