Home Rumble Youtube Twitter/X Kofi Contact / Crypto

Zen in Programming

Most software is complete garbage. The software is garbage because the code is garbage. The code is garbage because it lacks balance, discretion, and wisdom in how it was designed and written. It lacks zen because its developers lack zen. The developers lack zen because they aren't even aware it exists.

Progammers used to talk about zen all the time. There are books about it. There were entire websites dedicated to it. Now it's all about fads and cargo cults. Everybody argues about which whizz-bang language features are the best but nobody stops to consider if those features are even a net positive in the first place.


I once worked on a project at a job that had over 1600 unique NodeJS dependencies. It was a relatively basic website. My full time job was to wrangle these dependencies. Every week saw several packages recieve updates, some of which were critical security fixes and some of which would change the API or semantics and break our application (a basic website). A lot of the packages had pinned versions due to mutual imcompatibilities with other packages, which might also be pinned. The client that owned the website had already spent millions of dollars on it and didn't see why he should have to rebuild it despite having to pay several full time developer salaries to keep the big pile of shit running.

It had 3rd-party hosted microservices, and ran partially on Heroku, and partially on Apache, and also had a PostgreSQL database (with batch jobs), and also some CoffeeScript, and also a Redis server, and a RabbitMQ server too (if I recall), but most of the CoffeeScript had been converted back to JavaScript. There were promises and also node-style callbacks. It used underscore and lodash. The entire UI looked like it was cobbled together using random JS plugins by someone who had never coded in their life, and probably was because the company had a deal with some local "code bootcamps" to give minimum-wage internships to their completely unqualified graduates.

Had it been written in 2005 instead of 2013, it would have been a very simple PHP application running MySQL using jQuery on the front end. It would need almost no maintenance because it would have almost no dependencies. It would be faster too, not because PHP is faster than NodeJS (it isn't), but because it would be orders of magnitude less complicated. It would also be more stable, not because MySQL is more stable than PostgreSQL or Redis (it isn't), but because there would be only one database instead of a half dozen.


The latest fad is to slopcode everything. Proponents proclaim the incredible benefits of being able to shit out 20 times the amount of code as before. Anyone with even a modicum of zen immediately sees this as a massive regression. Less code is more, not more code faster. They are gleefully accumulating technical debt at rates never before seen. It took several years and millions of dollars to build up the mountain of cruft inside that website I maintained. Now it can be done in a few weeks and people are cheering this "accomplishment" as the End Of Programming (TM) or some nonsense. All developer jobs will be gone by 2024 2025 2026 2027.

6 more months, bro. just 6 more months. bro, please, bro. bro...


We don't have to jump off a cliff with the rest of them. We don't have to build supply-chain vulnerabilities into every single app with Rust and Cargo. We don't have to eat up 900 megs of ram to show a simple UI. We can just not use React. We can just not buy into the fads and cargo cults. We can write C just like in the old days, except with even more wisdom than the oldtimers had when they built Unix in a couple weeks.

Zen lies more in knowing what not to do. It's easy to make a big mess of things in any language, and some practically beg you to do it. Messy code is hard to follow. More precisely, code that is hard to follow is the definition of messy code. The harder it is to follow code the harder it is to modify it, the slower development goes and the less reliable the code becomes.

The next statement to execute should be trivial to determine

This is the core problem with exceptions, virtual function calls, operator overloading, function overloading, long jumps, and decorators. It's why goto was "considered harmful" and why function pointers should be used with utmost care. It's why macros have a bad reputation. It's why nobody uses Prolog.

Consider this line of C, found in the middle of some function: int x = y * z; You know exactly what it does and doesn't do, short of someone doing really retarded things with macros. y and z have to be some sort of essentially numeric types, and x is a 32-bit signed integer. This will result in at most one assembly instruction not counting loads and stores if necessary. There are no branches. Nothing secret happens because nothing secret can happen.

Now consider this line of C++, found in the middle of some function: int x = y * z; You have no idea what it does. y and z could be any type imaginable so long as there's an overloaded star operator between them that results in something numeric-ish. This line of code might have branches. It might allocate memory. It might make syscalls, spawn threads, write to the disk, wipe the TLB, segfault, or even shut down the computer. It might connect to the internet, download a full copy of Wikipedia, train an LLM on it, then feed the string values of y and z into the LLM to determine the result.

It might not even ever complete. It could very possibly throw an exception that effectively jumps to who knows where. And people thought goto was bad because you could jump around inside one function. You have no idea what this line does without first:

  1. Determining the true internal types of y and z, which might be a significant exercise unto itself.
  2. Determining if the star operator is overloaded for those two types.
  3. Determining which star operator overload will be used according to C++'s type coercion rules.
  4. goto 1; for the contents of the overload function, all the way down.
  5. Determining if y and z themselves are or are not modified in the process.
  6. Determining if any exceptions are thrown, and what types they are, and how far up the call stack they'll each get caught.

Now add in auto. Now chain this problem a dozen variables deep. Add in the fact that you can't grep half of the things involved. This is why C++ codebases are so hard to work on.

But you feel oh-so-clever overloading the plus operator, which brings us to...

Code should always do what it appears to do.

A bigger problem in the previous snippet is that * might not even be doing simple multiplication. It might be a dot product. It might be reducing a list. It might be function composition that modifies y, frees z, and returns a numeric status code about the success of the operation. It might be some ridiculous Straw Man argument which probably does actually exist in some crazy codebase somewhere. Now throw in auto and you can't even be sure what the result type is without going on an adventure. Again.

But muh infix notation. Gotta have infix notation. Can't possibly write mul(y, z) or dot(y, z) for composite types.

Functions should be easy to grep.

You shouldn't need advanced tooling to be able to work on the code. You should be able to use nano and grep if you want to. The code is too confusing if you have to have IntelliSense, even if you do have IntelliSense. Operator overloading is obviously a huge violator here, but regular overloaded functions are too, and also namespaces. Function names have to be unique in C, and any trickery using macros or _Generic exists in only one location and is itself easily grepped. There are static functions, but those have a keyword (hopefully) on the same line telling you they are static and not what you're looking for. (grep -v) C++ namespaces are declared and activated far away from the functions inside them or the code using them. You need a full-blown proper C++ parser to be able to tell which namespaces apply to which functions and lines of code. That parser might end up being your brain.

Once you resolve which namespaces apply, now you have to figure out which overload this call will use, which requires you to determine the types of the arguments, which as detailed before might be a wild goose chase. Throw in auto as the return value type and you have even less information to go on.

Virtual functions are violators too. Not only do you have to figure out the lexical type of the variable, you have to figure out which derived type it actually points to at runtime, which may be different in every invocation.

Explicit variable types help you understand the code.

A.K.A. "auto considered harmful"
You may have noticed a common thread in the previous points. auto makes everything worse. It's lazy and convenient to not type out some::Long<chain, of>::type::and::name_space<bullshit, For<every, fucking> > iterator but what is the true problem here? And even if you did only use it for iterators, which was the "think of the childen" excuse used to sneak auto into the standard, you don't have to use it for anything else. But people do. Constantly. Everywhere. Thank God you can't use it in argument lists or as a return type (yet). Apparently you can now, and it's completely fucked.

The main benefit of explicit types is that they let you easily reason about what's going on. This is the whole point of TypeScript. I don't know if TypeScript achieved its goal in practice, but the aim was noble at least.


We pick on C++ here but that's only because it's an unapologetic kitchen-sink language. Sure, Barney Starsoup had no zen when creating it, and the design committee seems to actively hate zen, but Muratori uses C++ extemely responsibly. You can too, if you have discipline. A lot of discipline.

The overriding principle is:

Just because you can, it doesn't mean you should.

Clarity is the single most important property code can have. If you can't understand it, you can't work on it. If you can't work on it, it might as well be a binary. At least then you know you can't work on it.

Following clarity is simplicity. Readable but complicated code is also hard to work on. Sometimes code needs to be complicated because the problem is inherently complicated or the solution demands performance which demands a complex algorithm. But code should be no more complicated than it needs to be. Know where performance is actually necessary and where you're overcomplicating the code to maybe avoid one string copy.

The kicker is that complicated code is usually slower than simple code. It makes sense when you think about it: less code means fewer instructions, less pressure on the decoders and instruction cache, and less memory bandwidth overall. Don't underestimate how much memory bandwidth and cache space the code itself can consume. Lots of programs run faster under -Os than -O3.