HN DigestHacker Newsletter, digested

Issue #809#Favorites

Simple Is Not Small

TL;DR

Following Rich Hickey, simplicity means parts that aren't braided together. That is a different property from size. Unix pipelines are small, yet their parts are coupled. A huge program like Google Drive can still be simple. So programs should aim for simple, even when that makes them bigger.

The article

  • A word-frequency count is short both as a shell pipeline and as a Clojure expression. To keep the words in original file order, Clojure needs a few more lines. The shell pipeline grows into temp files, `nl`, sorts and a `join`.
  • The culprit is `sort | uniq --count`. `uniq` only collapses adjacent lines, which ties counting to sort order. Separating the two forces table joins through text files.
  • In practice, "do one thing and do it well" is about size, not simplicity. Google Drive for Desktop is massive inside. Yet it asks the user for only three decisions.
  • A Rust struct ties type checking to a fixed memory layout. You can't iterate its fields without a proc-macro. Clojure's Malli schemas check plain maps at runtime. The schemas stay inspectable data, which makes a homemade doc generator easy.
  • Being small is a fair choice when the maintainer has few hours or little support. Coupling is almost never worth it. So programs should always be simple.
  • Decoupled tools like SQL and CSS can need huge efforts behind them, such as SQLite and Blink. The author's own coverage pipeline got larger after the fix. It also got simpler, with fewer hidden dependencies.

Why it matters

Read the original for the side-by-side shell and Clojure code, which shows the coupling better than a summary can. The Rust struct versus Malli schema example is also worth it. The author leaves taste, and how to build large systems without complexity, for later posts. So the how-to part is thin.

The discussion

The thread liked the thesis and Hickey's talk behind it, but went hard after the examples. Most of the energy went into rewriting the shell pipeline and arguing over what "simple" means. One very long exchange asked whether Clojure really decouples types from data.

The shell example is a straw man

Many readers posted better shell pipelines: an awk associative array, a pure bash loop, or words tagged with line numbers before `uniq --count`. They argued the shell tools work well within their own sort-and-accumulate paradigm, which runs in constant memory, and that banning awk is arbitrary. Others said the comparison mixes a process abstraction with a language macro, and that the Clojure version loads the whole file into memory. A few conceded that `uniq` really does couple counting to sorting, whatever the language.

The generally-useful point is that the classic shell utilities really do work pretty well if you're operating within their paradigm, which *isn't* "throw everything in a hash table". (That's the paradigm of later scripting languages.)

— @JoachimSchipper

The post is mixing up a lot of stuff because of a category error: conflating system design (pipelines) with language design (Clojure and Bash).

— @adityaathalye

"Simple" has too many meanings

One commenter split the word into three goals that often conflict: few tokens, easy to reuse, and easy to understand. Another mapped the essay onto "Worse is Better", which splits simple for the user from simple for the implementer. Developers play both roles. Others said familiarity distorts the judgment, and that "small" is a legitimate sense of simple in formal complexity theory.

There's at least three concepts we're all trying to stuff into the same word, and they are not only not "orthogonal" they are often in conflict with each other.

— @jerf

TFA maps "simplicity" to "MIT/Stanford simplicity" (simplicity for the user) and "smallness" to "New Jersey simplicity" (simplicity for the developer).

— @getnormality

Does Clojure really decouple types from data?

Twey and weavejester had the longest exchange, over the Rust versus Clojure example. Twey argued Clojure only moves the coupling, since it forbids any value without runtime type information and GC semantics. weavejester answered with messy medical data and said a Clojure function reading only white blood cell counts isn't coupled to the rest of the record. Both ended up agreeing Clojure is far more ergonomic for that kind of data, while Rust's traits reach similar results with more ceremony.

This is funny to me because seen from the other side, (this) Clojure couples runtime type information to data structures: you're no longer allowed to define a data structure that doesn't have some runtime type information attached.

— @Twey

This function requires patient to be a Patient struct, and therefore the function is implicitly coupled to every field in the struct, regardless of whether that field is ever actually used.

— @weavejester

Pipes are easy, not simple

Some readers defended pipes as simple when built well. Others listed the problems underneath: raw bytes and delimiters only, no types, and errors hidden unless you remember `set -o pipefail`. The missing types push people to jq and then to Python. Anyone driving pipes from a program has to handle exit codes, signals, buffering and partial writes.

Unix pipes are easy, and they are a very good abstraction, and their choice of abstraction boundaries is superlative, but they are not simple.

— @fwlr

But the moment you to do something that's a little different than the happy path it was built for, you need to go for another tool (jq) that has its own built-in pipe and small programming language, because Unix pipes won't cut it.

— @unscaled

Large and simple costs a lot

A few people with firsthand experience agreed simple-but-large is possible but very expensive. One developer building very modular software called it the hardest design job of his career, with 3 to 5 rounds per abstraction. That, he said, is why vibe coding works for scripts and fails beyond them. Others cited Brooks on the ninefold cost of a programming systems product, and noted small projects can't afford gigantic software at all.

Many aspects of this project have taken 3-5 rounds trying-and-trashing to get an abstraction that is uncomplicated.

— @hankbond

Consensus

Simplicity and size are different things, and Hickey's talk is worth watching. But the essay's examples were weak. Awk or a better shell pipeline does the word count without temp files.

Dissent

A minority called Unix pipelines simple and powerful for small problems, against the essay's claim. Some called the whole essay a Clojure advertisement with a flimsy definition of coupling.