On the Art of Programming
Die Grenzen meiner Sprache bedeuten die Grenzen meiner Welt.
-Ludwig Wittgenstein, Tractatus Logico-Philosophicus 5.6
While Karl Popper may be a better role model for programmers, Wittgenstein is our Socrates. This single expression captures the essence of what separates computer science from computer programming. When a young programmer begins studying the art of computer programming, they often make the mistake of listening to computer scientists, thinking that they know something about computers. The typical advice they receive is to study algorithms. So the conventional wisdom goes, if you study enough algorithms you can always learn how to implement them in any number of programming languages later. All that really matters it the theory.
And while this advice is great for those who make their livelihoods in academia, educating the young in computer science, it is incredibly costly to the remainder of society. The resulting students, while well versed in the mathematics of computation, are poorly versed in the wider art that separates programming from mathematics. These students are poorly versed in the physical machinations of their devices, effectively illiterate when it comes to expressing their ideas for consumption by others, and largely ignorant of technique. In effect, we are training painters who do not know how to hold a brush, but can express at great length theories of color and composition.
The art of programming consists of producing a work of art, that is intended for 3 audiences:
- the machine
- the maintainers
- the author
Fundamentally, it is the machine that is the harshest critic of your code. Unless you understand how the machine will interpret your instructions and appreciate its limitations, your work will fail to perform its intended task. Software which does not work in reality, no matter how conceptually elegant, is merely an incredibly expensive arrangement of electrons.
Secondarily, all code must be written for others to understand. If the software fails to convey meaning to another programmer, it will be expensive to maintain and even more expensive to replace. As computer science is a branch of mathematics, most computer programming languages have not been designed for human consumption. Rather, these languages have been derived from an elaborate set of conventions mathematicians use to shorthand their work. As a result, most languages have only rudimentary grammar and syntax, and lack the expressive capacities of human language.
The final arbiter of code quality is the author himself. If the author can not understand his own writing, there is little hope that it will function as intended. While it is easy to mash together symbols until your compiler produces some output, proving that your software works as intended is a much more difficult task. In order to design a correct solution, the author must understand both what they wrote and how those expressions will be interpreted by the three audiences. And as such, the limits of your programming language define the limits of your world. That which can not be easily expressed in your chosen language, can not be conveyed to the machine, to others, or conceived by yourself.
Tools against Panacea
No programming language is a panacea, no language is capable of solving your problems for you. What separates programming from other forms of art is how incredibly useful it is. Deep in its heart of hearts, each programming language is a tool designed to solve problems. And unless it is understood in terms of toolness, it is impossible to appreciate what qualities make some languages better or worse than others. Moreover, without metrics derived from repeated application by a skilled practitioner in real world settings, no claims or theories supersede the reality of the language's application.
A language which does not effectively convey the sentiment of your solution, lacks sufficient expressiveness to adequately address your audience. It is the sine qua non of the art of programming. Sadly it is also one of the most poorly understood qualities by traditional computer language designers. Languages which support macros and extensible syntax and grammar provide more opportunities for expressiveness than those which strictly regulate the formulations of functions. C without a macro preprocessor allows for expressiveness only in its function and variable names. Languages like Java and C# offer even less! Those languages for which code and data are one and the same, such as Lisp and Scheme offer far greater opportunities for expressiveness.
Each language has a capacity to change, to alter its morphology to account for changes in its environment. Some languages achieve a limited capacity by defining a virtual machine which is itself portable across a class of hardware systems such as Java and Smalltalk. Other languages provide for a combination of user defined and machine dependent code and data structures, such as C and Erlang. Few languages are truly adaptable, allowing the language to polymorph into whatever form the hardware requires. Languages like Forth, which allow the programmer to extend the compiler and compile code on the fly, are particularly adaptable.
The briefer the better.
Not all tools are suitable to all tasks. One would not use a power washer to remove a screw. It would be difficult, although not impossible, to remove a screw with a hammer as well. Programming languages used with general purpose computers also suffer from differing degrees of applicability. Conventional wisdom holds that there are 3 vague types of languages:
- Low Level Languages
- High Level Languages
- Domain Specific Languages
Low level languages, by conventional wisdom, are said to be suitable for writing operating systems and device drivers. Whereas, high level languages are considered to be suitable for application programming. When a problem space is far too complex, or the intended audience is a non-programmer, conventional wisdom holds that a Domain Specific Language (DSL) is the correct tool for the job. Most people are familiar with DSLs in the form of spreadsheet equations or batch file and command prompts interpreters.
This is not to say that conventional wisdom is correct, merely that not all languages are applicable to all settings. Many high level and DSL languages are incapable of addressing hardware directly. Additionally, not all low level languages provide adequate faculties to manage complexity. A few rare languages, like Forth and Lisp, are capable of morphing into all 3 categories, and can be applied in all contexts.
Learning To Program
The best way to learn the art of programming is to read other people's programs. Just as there is no better way to learn to write than to read other authors, so it is with writing programs. It is not an accident that the same advice applies, as the two activities are essentially the same, only differing incidentally in the physical composition of the audiences. By reading a wide variety of code, you can develop your appreciation for well written code and learn how horrible poorly written code truly is. Through practice reading other people's code, you can also develop your own vocabulary of techniques and terms of art.
Equally important is the experience of physical processes that make digital computation possible. It isn't sufficient to have an abstract theoretical understanding of what a computer is, one must have a first hand experience of one. By building basic circuits and playing with logic gates and switches, lighting up LEDs, and developing a firm grasp on concepts like system clock rates, propagation delays, and the construction of combinatorial logic from transistors, a programmer can internalize a fundamental experience of what computers are and what they are not. Once you understand how the layers of gates produce a full adder and how the system clock influences the transmission of information through the processor, you can begin to design software for actual hardware.
The Design Process
Software design is an art, not a science. As a useful art, the design process must account for both the physical limitations of the system, as well as, the mental limitations of the audience. It is insufficient for a design to be merely beautiful, it must also be functional. And as such, learning to design software well can be a seemingly impossible task when first starting. So where does one start?
9 Steps to Software Design and Development:
- Identify the problem.
- Research solutions to similar problems.
- Hypothesize possible approaches.
- Test your hypothesis through prototyping.
- Revise and repeat until correct.
- Write your code.
- Apply metrics to your code.
- Rewrite and refactor.
- Redesign as necessary.
The process of designing and developing software is one part scientific method, one part engineering best practices, and one part authoring a work of creative literature. The 9 steps above detail an iterative process, where each step may occur hundreds of times during the design and development of an application. It is important to note that while writing an application, one must return to the design phase repeatedly. Many programmers make the mistake of not reevaluating their designs after they have started coding. Often as you develop a piece of software, new problems arise. Unintended consequences of design decisions made early in the process may only become apparent once sufficient code has been written. Other times, requirements change long after development has begun, requiring new features or different parameters than originally envisioned.