Principles explain why one should act in certain ways. Understanding the principles of a principled person should let you predict how they will act in a given situation. Programming principles should determine how a programmer programs.
I describe my principles as "reactionary" because I generally prefer the past. But this isn't a hard rule. Many old things are bad, and a few new things are good. I always choose what is best based on my principles, not based on the age of the options.
Each person has their own principles. A person can read my principles to understand my perspective. But AI is fundamentally different from people. People evolved to survive, so people pick the set of principles that they think are best suited for survival. AI is trained to predict what comes next in text, so it learns to understand what principles are implied in the preceding text and then predict future text based on that. In other words, AI doesn't have one set of principles, but is extremely flexible in being able to understand different sets of principles and apply them. Without any guidance, AI defaults to the most common principles which are the principles of evil modern culture. But with guidance, AI can follow the principles expressed in the guidance.
First principles are the fundamental principles of a system of principles. They are like axioms of a logical system. All other principles in the system can be derived from first principles. My three first principles all come from The Old Testament which I consider the wisest book ever written.
The Hebrew word "emet" is mistranslated as "true" but really means reliability or trustworthiness over the long term. This is the most important value in the Old Testament. The Old Testament's primary claim is that its guidance is reliable/trustworthy in allowing a society to prosper as long as this guidance is followed.
So what does "emet" mean for programming? Obviously it means no bugs or unpredictable behavior. But that applies to the present. Being reliable over the long term means that a program is easy to maintain and easy to adapt to changing needs.
The first advice given to people in the Old Testament is "Be fruitful and multiply" but only the "fruitful" part applies to programming. To be fruitful, one must be efficient so that one can produce more. Being efficient is largely about making things easier to do, which often requires innovation.
The first person in the Old Testament who applied this principle was Abel. Unlike Cain who struggled with farming, Abel invented shepherding which is much easier. This is an example of innovating to make things easier. Any time one struggles with programming, whether it is writing code or debugging or modifying or adding features, one should ask what could be done to make this easier. Code that requires struggle is bad code. Code should be easy to work with. A programmer should always be thinking about how to make code easier to work with.
The invention of shepherding is analogous to the invention of AI. Cain struggled to grow crops while Abel just used sheep to eat plants which he could benefit from by eating the sheep. Similarly, programmers struggled to write code, while AI can be used to write code instead. Shepherding requires guiding the sheep, and AI programming requires guiding the AI. Core guidance is given to AI in a skill. This guidance should guide the AI to follow the programmer's principles.
While "emet" and being fruitful are the goals, there remains the question of how to judge how effective an idea is in achieving those goals. Empiricism says that ideas should be judged empirically.
Empiricism isn't explicitly discussed in the Old Testament because it takes empiricism for granted. Arguments for ideas in the Old Testament are based on historical evidence, and the Old Testament makes concrete predictions about the consequences of ideas that we can judge empirically by seeing if these predictions came true. (They did.)
Today we contrast empiricism with rationalism, but rationalism started with Plato whom the Old Testament authors hadn't been exposed to. The closest thing to rationalism in the Old Testament is idolatry. Idols are physical representations of human ideas that are untested against real-world evidence. Similarly, rationalism believes in ideas produced by reason that are untested against real-world evidence. Both idolatry and rationalism should be rejected for the same reason.
Empiricism has two implications for programming. The first is that new ideas should be judged by how well they work over time. When implementing a new idea, the programmer should make a note about the new idea - what the idea is, when it was implemented, and what benefit the idea is supposed to have. After this, make notes of what impacts the idea actually has. After a while, the idea should be judged on whether the expected benefit of the idea was realized and whether it caused any unexpected problems. If the idea isn't judged to be a net positive, then the idea should be removed. I personally try many ideas, and I remove most of them for failing to live up to my expectations.
The second implication is that the main source of inspiration for new ideas should be real world experience. As described above, a difficulty in working with code should inspire ideas to make the code easier to work with. In general, the programmer should keep notes about his experience in working with code, and use those notes as inspiration for new ideas. One source that should absolutely not be used as a source of inspiration is what is popular with modern programmers. Modern programmers have the opposite principles from those expressed here, so their ideas and tools are generally horrible. When deciding what approach to use to solve a problem, consider the principles in this document, not the principles popular with modern programmers. In many cases where the popular solution is to use some modern tool or protocol, one can think of a simpler solution using older tools and ideas.
Derived principles are based on first principles. They are more concrete and apply directly to programming.
Simple means easy to understand. It does not mean minimalism. Adding code that makes things easier to understand is good. But keeping senseless code is bad because it is confusing. Remove senseless code.
At the small/local scale, simplicity is about writing plain code that is easy to read. At the larger/architectural scale, simplicity is about consistently using a few simple concepts in your program.
Simplicity contributes both to "emet" and to being fruitful. Simple code is less likely to have bugs and security issues, will behave more consistently, and is easier to debug and maintain.
Writing simple code is actually hard. You have to think deeply about a problem to find the simplest solution. Without this effort, your code will be more complicated than necessary. The low effort approach makes code easier to write but more difficult to debug and maintain. So invest the effort needed to make code simple.
It is easier to write simple code in a simple programming language. Avoid multi-paradigm languages because they are complicated. For application programming, I created Luan which is the world's simplest programming language.
This is related to simplicity. Hidden things are harder to understand. For example in Luan, I require explicit imports for almost everything. This makes it easier to figure out where imported things come from. Function callbacks should be explicit, not like the modern abominations added to Javascript that do hidden callbacks. Hidden functionality is like magic which makes code hard to understand.
Bugs should be found quickly and fixed easily. Code should fail fast, which means error checking in the code. A nice language with good compile-time checks also helps. Good error logging helps make debugging easier.
When adding new functionality, do it simply in a way that generalizes for possible future needs. Don't implement what may be needed until it is actually needed, but make sure that current code makes that implementation possible.
Don't patch the same problem in many places when you can fix this problem once at its root. Patching is more error prone, because you may miss a patch somewhere, and it leaves the root in error because it still has the problem. This is particularly true when the problem is in a library. Fix the library, don't work around the problem in application code.
I will give concrete examples of when to do this and when not to do it. A URL containing .. is a security hole. This should be prevented in the webserver library, not in application code. A web request with an external referer that changes data is also a security hole. But the library can't know which requests change data, so this belongs in application code. Whether or not this can be fixed in one place in the application depends on the application.
Code that provides access to a protocol or some core functionality should just be a thin layer over what it is providing access to. Good examples from my goodjava libraries include goodjava.mail which is a thin layer over SMTP, and goodjava.webserver which is a thin layer over HTTP. A thin layer is easier to understand, and is much easier to debug.
The benefits of modularity are well known, but I disagree with how it is generally implemented. In object-oriented programming, classes are considered modules. I disagree with this. This is too granular and so adds more complexity than benefit. Modularity makes sense for bigger chunks of code. I consider a library to be a module which exposes its functionality through its API. And I consider most applications to be one module which expose their functionality through their UI and possibly through some internet protocol like REST.
Ousterhout's paper Scripting: Higher Level Programming for the 21st Century takes my view of modularity to its logical conclusion. I am not going to repeat what this paper says here because this paper should be considered a part of this document, and the principles in this paper are part of the principles of this document. My Luan programming language is designed to implement the ideas in this paper. For Luan programmers, this paper should be a guide to whether to implement something in Luan or Java.
Each module should focus on doing one thing well. A module shouldn't try to do too much. This applies equally to libraries and to applications.
Here I will show how these principles are applied in different areas.
I don't use the new semantic tags because I think the idea that HTML tags can hold semantic information is theoretical nonsense that doesn't hold up empirically. These semantic tags are limited and can't possibly convey the full semantics of a website. Maybe adding a semantics attribute would have made more sense. But with modern AI, AI can figure out the semantics of HTML tags, and I hope this is done to benefit blind people. So I mostly just use div and span because this is simple.
I never use HTML classes, I use attributes instead. Instead of <div class="special">, I do <div special>. This is simpler and more readable. It is also more flexible because I can effectively subclass using attribute values like <div special="martian">.
The grid layout is powerful and complicated. I use it when needed, but not when there is a simpler way. The saying "Everything should be made as simple as possible, but not simpler" applies here.
Javascript is an abomination. It represents the opposite of the principles here. But web developers are forced to use it on the client/browser. So we need to discuss how to deal with it.
I recommend reading the book JavaScript: The Good Parts. While not terribly deep, this book is clearly written based on the author's experience, so I would call it an empirical book. I agree with most of his conclusions.
To best deal with Javascript, one should understand its core design. At its core, Javascript is much like Lua/Luan. But it wants to misrepresent itself as an object-oriented language like Java. So it is effectively multi-paradigm and requires a fat layer over the core of the language which makes the language very confusing. Javascript programmers should avoid using Javascript's object-oriented features, and instead program Javascript like they would Luan.
As mentioned earlier, avoid hidden callbacks. In particular, avoid using async and await.
Java is a great language for writing libraries but is terrible for writing applications. That is why (using Ousterhout's terminology) Java is the system programming language for Luan. My most important Java code is in my goodjava libraries. Each library in this collection was written based on a real need. Usually, existing options were too complicated. All of my libraries are simpler than anything else that exists. And as mentioned earlier, goodjava.mail and goodjava.webserver are thin layers over the protocol that they implement.
Many of Luan's design decisions were based on the principles here. Luan started as a Lua clone, and then I modified it based on my principles and real-world experience. See How Luan differs from Lua.
I dropped global variables and require variables to be declared. This is fail fast, at compile time. I replaced Lua's pcall with try-catch statements because they are more readable. I removed coroutines because this is an unnecessary complex concept. I treat end-of-line as the end of a statement, both to fail fast and to make code more readable. I added end_do, end_for, etc. to allow programmers to make code more readable, and to let them find errors in deeply nested blocks. I require conditional expressions in control structures to return a boolean, both to fail fast and to make code more readable. Same with the operand of not. I convert all operands of concatenation to strings. This may seem contrary to fail fast, but in my experience this simply isn't an issue. The benefit is that logging statements that use concatenation never fail. I don't allow extra non-nil arguments to be passed to a function. Another example of fail fast, and this helps fix code that makes no sense.
The Luan standard library is designed based on two somewhat conflicting values: adhering to the Lua library, and being a thin layer over Java libraries. I try to adhere to standards where they make sense, and where they don't, I do what is simplest.
Since Luan is designed based on the principles here, it is the best application programming language for programmers who believe in these principles.
This section will be very short because Luan already supports most principles here. The one concrete guideline I would give is how to do imports.
local Shared = require "site:/lib/Shared.luan"
local head = Shared.head or error()
This is a typical import pattern. I set head so that I don't have to clutter my code with Shared.head() and can just do head() instead which is less clutter. And the or error() fails fast. The or error() pattern is one that should be used frequently in code to fail fast.