Brian Kernighan wrote the permanent version of this post in 1978, in two sentences:

Everyone knows that debugging is twice as hard as writing a program in the first place. So if you’re as clever as you can be when you write it, how will you ever debug it?

The argument here is narrower and more uncomfortable: functional style invites the violation of Kernighan’s law more seductively than most styles, precisely because of what makes it good. Everything is an expression; expressions compose; composition compresses. The distance between “this pipeline is clear” and “this pipeline is a party trick” is three refactors, each of which felt like an improvement to its author. And the cost lands elsewhere — on the reviewer, the on-call engineer, the next hire — which is what makes it hidden. Nobody budgets for it because the person who incurs it never pays it.

This blog spends most of its time arguing for functional techniques. This post is the ledger’s other column, from the section where opinions live.


What cleverness looks like when it wears a lambda

Cleverness is not density, and it is not abstraction. A map/filter/fold chain your whole team reads fluently is dense and clear. Cleverness is spending the reader’s attention on your ingenuity instead of the problem. Three specimens, all from real Java codebases in spirit:

The tuple smuggled through reduce. One pass, two aggregates, zero names:

BigDecimal[] stats = orders.stream()
.map(Order::total)
.reduce(new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO},
(acc, t) -> new BigDecimal[]{acc[0].add(t), acc[1].max(t)},
(a, b) -> new BigDecimal[]{a[0].add(b[0]), a[1].max(b[1])});
// stats[0] is the sum. stats[1] is the max. You just have to know.

It compiles, it runs, it is even efficient — one pass! It is also wrong: BigDecimal.ZERO is not an identity for max, so a list of all-negative totals — a day of refunds — reports a maximum of zero, a value that appears nowhere in the data, and an empty list is indistinguishable from it. The bug sat comfortably inside the cleverness, which is the point: every reader must first decode the array-as-tuple convention, track which index means what, and check the combiner against reduce’s identity contract before they can even see it. The boring alternative has nowhere for it to hide:

BigDecimal sum = orders.stream().map(Order::total).reduce(BigDecimal.ZERO, BigDecimal::add);
Optional<BigDecimal> max = orders.stream().map(Order::total).max(Comparator.naturalOrder());

Two passes over an in-memory list cost nothing worth discussing at the scale most services meet, ZERO is a true identity for add, and max’s empty case surfaces as an honest Optional — while for the sum, zero really is the sum of nothing. The clever version optimized a machine cost nobody had measured by adding a human cost everybody pays — and a wrong answer nobody would spot in review. When one pass genuinely earns its keep — the list is huge, Order::total is expensive, the stream cannot be replayed — the answer is still not the array: a small record with an explicit empty case states its meaning, or, for a genuinely anonymous pair inside one private step, the library’s tuples guide blesses a Tuple2. The named record earns its letters here because sum and max share a type and will be confused; the guide’s decision table draws exactly that line.

The domain fact encoded as a type pun. The type system can express “not yet evaluated” versus “evaluated, nothing applies” by nesting — here with the library’s Option, though java.util.Optional invites the identical pun:

Option<Option<Discount>> quote; // none = not evaluated; some(none) = no discount applies

That comment is load-bearing, which is the tell: the encoding means nothing without it. Nested containers are a fact about plumbing; the domain had two named states that deserved names of their own:

sealed interface Quote {
record NotEvaluated() implements Quote {}
record NoDiscount() implements Quote {}
record Discounted(Discount discount) implements Quote {}
}

Same information, but now the compiler enforces the cases, the switch reads as policy, and no one needs the comment. The clever version signaled fluency with the container types; the boring version made the API say what it means.

The pipeline that swallowed the paragraph. Chains of flatMap and fold where each step is small but the whole has no name anywhere — combinator golf. Each link is defensible; the chain, read at 2am, requires the reader to run the substitution model in their head for eight steps while holding three type parameters. The worse post already draws the concrete line — a chain of more than three steps benefits from named intermediates — and this specimen is what ignoring that line looks like at expert speed. The fix costs one private method with a domain name per conceptual step — the pipeline shape survives, the archaeology doesn’t.


Why the cost stays hidden

The economics are asymmetric in three ways, and all three favor the writer:

  • The cost is paid at read time, and code is read far more often than written. The clever fold took its author four satisfying minutes. Every future reader pays a decoding tax, and the sum of those taxes exceeds the writing time within weeks.
  • The cost lands on other people. Review latency is the first symptom — the PR that sits because reviewing it honestly requires an hour nobody scheduled. Approvals arrive anyway (“looks right, I think”), which converts unread cleverness into unaudited risk.
  • The cost compounds silently. The clever module becomes the one nobody volunteers to touch; the team routes around it; the person who wrote it becomes its permanent operator. That is a bus factor of one, self-inflicted, wearing the costume of mastery.

There is also a social engine underneath, worth naming because naming it weakens it: terse functional code signals membership. Point-free style, the maximally general combinator, the one-expression method — these read as fluency to insiders and as a wall to everyone else. When the signal matters more than the reader, the codebase is paying for someone’s identity. It is kin to purism losing to pragmatism in teams that ship — though that post makes the case on technical trade-offs; the social column is this one’s addition.

To be precise about the boundary: this is not the cost of functional abstraction — learning what a fold is, reading Result pipelines — which is a real but different ledger, one this blog will take up on its own. Cleverness is the spending you do after fluency, and it is entirely optional. The antipatterns post catalogs the shapes that slip into otherwise careful code by accident; this post is about the narrower subset written on purpose — cleverness as a choice, made at the keyboard, with the better version in plain view.


Spending ingenuity where it pays

The prescription is not “dumb it down” — boring code is not unsophisticated code. Predictability is the sophistication. Some working heuristics, offered with opinions attached:

  • Name the intermediate thing. A local variable with a domain name is documentation the compiler checks. If a pipeline needs three of them, that is not a failure of functional style; it is the pipeline telling you its structure.
  • Prefer two obvious lines to one clever one. You will win the exchange every time someone reads the code — which is to say, every time the code matters.
  • If writing it was fun, read it again tomorrow. The fun is a signal: you were solving a puzzle. Confirm the puzzle was the domain’s, not one you smuggled in.
  • Let the team’s fluency set the density. An idiom everyone reads at a glance (filter/map/toList) is free; an idiom one person reads is a tax on four. This boundary moves as the team learns — push it deliberately in teaching moments, not incidentally in a Friday PR.
  • Spend cleverness on the domain, not the plumbing. The insight that two pricing states deserved distinct types is cleverness that pays — every reader afterward thinks more clearly. The array-tuple fold is cleverness that costs. Same ingenuity, opposite sign; the difference is whether the reader ends up smarter about the problem or about you.

Kernighan’s law has a constructive corollary: write at half your cleverness and you retain a margin for debugging — and for every future reader who meets the code without you standing next to it. A sailor’s knot is judged at the wrong moment to impress: not when it is tied, admired, and photographed, but when it must be untied — in the dark, in weather, under someone else’s cold hands. The rope in the photo above holds its rail beautifully; the only test that counts is the untying. Tie your code for that moment.


Further reading


Found a bug or have a suggestion? Open an issue on GitHub.