Goodnight Wiki / Language Design Philosophy

Language Design Philosophy

Programming languages are not just tools — they're philosophies made executable. Every language embeds a set of beliefs about what matters: simplicity or correctness, implementation ease or interface elegance, getting it done today or getting it right forever. These beliefs propagate virally through the software that gets written in them.

Worse Is Better

In 1991, Richard Gabriel published an essay that became one of the most cited arguments in software history. He laid out two design philosophies — the "MIT approach" and the "New Jersey approach" — and argued, somewhat reluctantly, that the worse one wins.1

The MIT approach (which Gabriel called "the right thing") prioritizes interface simplicity over implementation simplicity, insists on correctness, demands consistency, and pursues completeness. Common Lisp and Scheme are its exemplars. The New Jersey approach (which he called "worse is better") flips the priority: implementation simplicity comes first, correctness is slightly negotiable, consistency can be sacrificed, and completeness is the first thing thrown overboard.

The canonical example is the Unix PC loser-ing problem. When a system call is interrupted, the MIT approach says the kernel should transparently restart it — the user program shouldn't have to know about interrupts. The New Jersey approach says the kernel should return an error code and let the user program retry. The first is the right thing; the second is simpler to implement. Unix chose the second, and the user program has to wrap every system call in a retry loop.

Gabriel's insight was that the "worse" approach has better survival characteristics. A system that's 50% right but runs everywhere will spread like a virus, condition users to expect less, and then be gradually improved toward 90%. A system that's 100% right but only runs on the most sophisticated hardware will stay niche. "Unix and C are the ultimate computer viruses."1

C is the archetypal worse-is-better language. Dennis Ritchie designed it in parallel with Unix at Bell Labs between 1969 and 1973, and the two shaped each other profoundly. C's type system was born not from theory but from necessity — BCPL and B were typeless languages, and the arrival of the PDP-11 with its byte-addressable memory forced the introduction of char and later a full type structure. The crucial innovation — arrays decaying to pointers when used in expressions — was a hack to make existing B code continue working while enabling structs. It was not elegant, but it was workable, and the language spread.2

The essay is more nuanced than people remember. Gabriel wasn't simply arguing that worse is better — he was worried about it. He wanted Lisp to win but understood that it wouldn't, because the diamond-like-jewel school of design produces languages that are forever almost finished, while the viral school produces things that are ugly but everywhere.

Zero-indexed arrays are a perfect miniature of the same dynamic. Ask most programmers why arrays start at zero and they'll say "pointer arithmetic" — but that's a post-hoc rationalisation. BCPL arrays were zero-indexed before pointers or structs existed. Fortran used arbitrary index ranges. Algol 60 used one-indexing. By the early 1960s there were three competing conventions, and zero won not through technical superiority but through lineage: BCPL begat B begat C, and C conquered the world. The "elegant" Dijkstra argument came decades after the decision was made. The actual person who made the choice — Martin Richards, designer of BCPL — did it because it was slightly simpler to implement on the machines available to him in 1967. One implementer's convenience, frozen into a convention that billions of lines of code now depend on.3

The Seven Ur-Languages

Beneath the thousands of programming languages in use today, there are only about seven fundamentally distinct families — what Fred Becker calls "ur-languages." Each one represents a different set of neural pathways, a different way of thinking about computation. Learning a new language within the same family is easy; crossing to an unfamiliar family requires genuinely new mental infrastructure.4

The families are: ALGOL (sequential statements, loops, functions — C, Python, Java, and nearly everything mainstream), Lisp (code as data, macros that rewrite the language itself), ML (first-class functions, algebraic types, Hindley-Milner inference — Haskell, OCaml), Self (objects sending messages, live environments — Smalltalk, and JavaScript via its prototype system), Forth (stack-based, word definitions, extreme terseness — PostScript is a Forth), APL (everything is an array, operators are single glyphs, programs so terse they become their own labels), and Prolog (facts and rules, execution as search, programs as logic).4

What's interesting is how the families cross-pollinate over time. ALGOL languages have steadily absorbed features from ML (pattern matching, algebraic data types, type inference are appearing in Rust, Swift, and even Java). Lisp's macro system shows up in a weakened form as C++ templates and Rust's macro_rules!. Every modern language has closures, which came from Lisp via ML. The ur-languages themselves are stable, but the borders between their descendants keep blurring.

The classification also reveals blind spots. Most programmers only ever learn ALGOL-family languages, which means they only ever think in one paradigm. The payoff of learning an unfamiliar ur-language isn't the language itself — it's the new neural pathways. Learning Prolog changes how you think about search problems even when you're writing Python. Learning APL changes your sense of what "a loop" should look like.

The Lisp Curse

Rudolf Winestock identifies a paradox that cuts deeper than worse-is-better: Lisp is so powerful that problems which are technical issues in other programming languages become social issues in Lisp.5

The thought experiment is simple. Adding object orientation to C requires the programming chops of a Bjarne Stroustrup — it's genuinely hard, so only two serious attempts (C++ and Objective-C) ever gained traction. For any given platform, the question of which object system to use has been answered definitively. But adding object orientation to Scheme is a sophomore homework assignment. So in the 1990s, there was a warehouse inventory of OO packages for Scheme, each one a lone-wolf project solving 80% of the problem (a different 80% in each case), poorly documented, non-portable, and liable to be abandoned when the maintainer got a real job.

The curse scales up. Dr. Mark Tarver wrote Qi, a dialect of Lisp that implements most of Haskell's unique features — type inference, pattern matching, lazy evaluation — in under ten thousand lines of macros. In a world where teams of talented academics were needed to write Haskell, one person did it alone. And that's exactly the problem. The expressiveness that makes this possible also makes collaboration unnecessary, which makes standardization impossible, which means the ecosystem fragments into a thousand brilliant, incompatible, undocumented personal tools.5

The Lisp Curse is Worse is Better's evil twin. Gabriel showed that worse implementations spread faster. Winestock shows that more powerful languages fragment faster. Both mechanisms punish the "right thing" — but from opposite directions. C++ won because C was too hard to extend individually. Lisp lost because Lisp was too easy. The Curse is also the ally of Worse is Better: when you can hack Emacs to get something good enough for yourself, you never build the thing that would be good enough for everyone.

This isn't just historical. It explains why Erlang's OTP framework succeeded where Lisp's ecosystem fragmented: Erlang is powerful enough but constrained enough that the community converged on one way to build fault-tolerant systems, rather than a hundred individual approaches.

The Zen of Erlang

Erlang represents a design philosophy orthogonal to the worse-is-better axis: embrace failure as a building material. The "let it crash" motto sounds insane until you understand what it means in practice — not uncontrolled failure everywhere, but turning crashes into tools through isolation, supervision, and restart.6

Erlang processes are fully isolated (no shared memory), extremely lightweight (thousands are normal), communicate only by copying messages asynchronously, and are preemptively scheduled. Links and monitors let you codify dependencies between processes: when a process dies, linked processes receive exit signals. "Trap exit" processes can catch these signals and restart the dead process. This gives you supervision trees — hierarchies where stable, critical infrastructure lives near the root and fragile, moving parts live at the leaves. Like real trees: the leaves fall off in autumn, but the tree survives.6

The deeper insight is about the nature of bugs in production. Jim Gray distinguished Bohrbugs (repeatable, easy to find in testing) from Heisenbugs (transient, manifesting once in a billion executions). The repeatable bugs get caught before shipping. What's left in production is overwhelmingly Heisenbugs — race conditions, resource exhaustion, cosmic-ray bit flips, configurations nobody tested. Restarting a process from a known good state fixes most Heisenbugs, because the transient conditions that triggered them are unlikely to recur. It's not a hack; it's a strategy matched to the actual distribution of production failures.6

Erlang's design also argues against the myth that parallel programming is hard. As Chisnall points out, Alan Kay taught an actor-model language to young children who wrote programs with 200+ threads. Erlang programmers routinely build systems with thousands of parallel components. What's hard is parallel programming in languages with C's shared-mutable-memory model. The difficulty is not inherent in parallelism — it's an artifact of the abstract machine.6

The Tension in Language Evolution

C's evolution from BCPL illustrates a pattern that repeats throughout language history: practical constraints drive design decisions that become entrenched idioms. Thompson's B was "BCPL squeezed into 8K bytes of memory and filtered through Thompson's brain."2 The B compiler generated threaded code because the PDP-7 was too small for real compilation. The ++ and -- operators were probably suggested by the PDP-7's auto-increment memory cells, not the PDP-11 (which didn't exist yet). The = for assignment instead of := was a matter of taste inherited from BCPL via PL/I. Each of these was a micro-decision that shaped a language now used by millions.

Rust represents the opposite philosophy from C — it's an attempt to build a systems language that is the "right thing" while remaining practical enough to spread. Its borrow checker imposes rules from affine logic (each value used at most once unless explicitly copied) that make memory safety a compile-time guarantee rather than a runtime prayer. But as bunnie Huang noted after writing 100K lines of Rust for a security-focused OS, the language "is not simple" — its std library alone represents a vast hidden attack surface, its syntax is dense to the point of line noise, and its six-week release cycle means the language isn't finished yet.7

The Curry-Howard correspondence tells us that every type system corresponds to a logic system, and every logic system suggests a type system. Rust's type system corresponds to affine logic. Haskell's corresponds to intuitionistic logic. C's type system is so weak it barely corresponds to anything — which is exactly why it's so permissive, and exactly why bugs in C are so devastating.

Go is the most instructive contemporary case study in worse-is-better, because it reveals what happens when a language team doesn't actually want to design a language. As Amos Wenger argues, what the Go team really wanted was a great async runtime — and they needed a language to write TCP, HTTP, TLS, and web services on top of it. The language "just happened," borrowing from C, Java, and Python to be familiar to Googlers fresh out of school.8

The result inherits the worst of all three parents. From C: no concern with error handling, everything is mutable state, "just be careful." From Java: the distinction between values and references is erased, so you can't tell from a callsite whether something is getting mutated. From neither: no sum types, so modeling "either an IPv4 or IPv6 address" is painful; no immutability, so preventing mutation requires handing out copies and being very careful; zero values for everything, which means nil channels block forever, sends to closed channels panic, and forgetting to initialize a struct field compiles cleanly. Wenger's observation that Go's response to all of these is identical to C's — "just be careful" — is the sharpest indictment. A language designed in the 2010s repeats the design non-decisions of the 1970s.

Go is also an island: its custom toolchain, calling convention, and linker mean the only good boundary with Go is a network boundary. Calling C from Go requires manual descriptor tracking; calling Go from anything else means shoving the entire Go runtime into your process. The practical cost of this insularity is that decades of institutional knowledge about debugging, memory checking, and interoperability tools simply don't apply. These aren't bugs — they're the consequence of choosing to live in the Plan 9 cinematic universe. The deeper problem is cultural: because Go makes it impossible to solve certain categories of problems at the type level, the community has adopted a posture that those problems aren't worth solving at all. "You can't prevent all bugs, so why try to prevent some?" is the fallacy that Wenger identifies at the heart of Go culture, and it's worse-is-better taken to its logical, corrosive endpoint.8

The deepest lesson from the history of language design is that there's no free lunch. Worse-is-better languages spread fast but accumulate decades of technical debt (C's undefined behavior, JavaScript's type coercion). Right-thing languages are beautiful but slow to arrive and slow to adopt. The most successful languages of the last decade — Rust, Swift, Kotlin — seem to be attempting a synthesis: right-thing ambitions delivered in a worse-is-better packaging, gradually adding correctness features to ecosystems that are already viral.

Footnotes

  1. The Rise of Worse is Better by Richard P. Gabriel — source 2

  2. The Development of the C Language by Dennis M. Ritchie — source 2

  3. Citation Needed by Mike Hoye — source

  4. The seven programming ur-languages by Fred Becker — source 2

  5. The Lisp Curse by Rudolf Winestock — source 2

  6. The Zen of Erlang by Fred Hebert — source 2 3 4

  7. Rust: A Critical Retrospective by bunnie Huang — source

  8. Lies we tell ourselves to keep using Golang by Amos Wenger — source 2

Open in stacked reader →