1
A Brief Account of the Origin of Computer Programs
To “compute” is often used synonymously with “calculate,” but that is misleading. This confusion may be responsible for the popular myth that those hoping to become programmers need to be good at math, which in turn deters many from pursuing the craft. While computers did indeed inherit their name from people who manually compiled logarithm tables, they do something far more profound than that.
The theory of computation is the theory of how to use physical objects to represent abstract ones. One does not need to build a machine to do this. For example, when children learn to count the integers, they use fingers to assist with memory: every finger represents a number. (All thinking is computation, but I will get to that later.) We loosely model our present-day computers after universal Turing machines. A universal Turing machine takes this ability to represent abstract objects using physical ones to the universal level. It can simulate any abstraction in arbitrarily fine detail, and all universal Turing machines share this repertoire (that is the kind of universality referred to in the name). Via those abstractions, a universal Turing machine can simulate any physical process. To simulate something in this sense is not to “fake” it; the information processing is the same.
The set of all possible motions in a Turing machine, which is equal to the programs that one can run on it, is in one-to-one correspondence with the set of all possible motions of anything. So when a software engineer writes and then runs a computer program, he instructs that computer to move its internals in precisely such a way as to simulate a particular set of abstractions. He instantiates abstract objects and their relationships in physical objects and their motion. Therefore, writing programs is always about simulating abstractions and finding those instructions that will cause the computer to move its internals physically as is required. In a way, software engineers are physicists.
Why do software engineers write programs? Programming, like any creative endeavor, always starts with problems. Perhaps one of the first programming problems was how to automatically calculate logarithm and cosine tables for use in navigation and other areas. Large numbers of people – “computers” – compiled these tables. They contained errors, which cost lives. If this process could be automated using a machine, fewer errors might find their way into the tables.
A tentative solution to this problem was the difference engine. It was first conceived of by the German engineer Johann Elfrich Müller in the late 1700s, and again by the English polymath Charles Babbage to address the problem of dying seafarers in the early 1800s. Unfortunately, neither of them ever built it. Later on, Babbage envisioned the first universal computer, which he called the “analytical engine.” The English mathematician and associate of Babbage’s, Ada Lovelace, even wrote one of the first computer programs.
A problem, in this sense, is not necessarily negative. It is a conflict between two or more ideas. Babbage recognized conflicts between the actual and desired accuracy of logarithm tables used for seafarers; and also, on a more tragic note, between the desire to save their lives and the reality of their deaths. Today, we write programs to solve all kinds of problems: instant messengers solve the conflict between the desire for real-time communication and the reality of the slow speed of snail mail. Facial recognition solves the conflict between the need for faster identification and the reality of clunkier mechanisms of identification, such as usernames. The universality of computation implies that one can solve all soluble problems by writing the requisite software.
While developers write all of their programs to solve problems, merely solving a problem is not enough. Not every solution will do. It has to be a good solution: a good program implementation. There is an objective difference between a good and a bad implementation: a program has a good implementation when it is adapted to solving the problem it purports to solve. For it to be adapted means that few changes would make it perform better at solving its problem, and most changes would make it perform worse at that purpose. Every part of the implementation plays a vital role in solving the problem, and changing any part would break its ability to do that. In other words, the program is hard to vary while solving the problem it purports to solve., This means a good implementation resists change. All popular software-engineering principles, such as modularity and reusability, are special cases of the globally applicable principle of being hard to vary.
The origin of every program is the programmer’s creativity. He solves problems by first jumping to any solution and then trying to improve it. During this process, he tries out many different implementations. All he can do is invent many solutions, and then eliminate the bad ones. There is no other way. This process is evolution: trial and error correction. The philosopher Karl Popper also referred to it as conjectures and refutations (though he did not only apply this to software engineering but knowledge creation in general). A programmer guesses solutions, criticizes them, and picks the best remaining one – if there is one. If there is not, he has to guess more candidate solutions and criticize them once more, until he has found a tentative solution. That is creativity, and it fuels all human problem solving and knowledge creation, not just software engineering. Since a good program implementation is literally the result of evolution, it is no metaphor to consider it well adapted to solving a problem.
A bad implementation is easy to vary while still solving the problem it purports to solve. It may solve a problem, but it does not solve it well. The worse the implementation, the easier it is to make improvements to it, though this always requires additional creativity. Being easy to vary can go two ways. Either a program is easy to vary internally, meaning parts of the program can easily be omitted or changed to improve it, or it is easy to vary externally, meaning it is not adapted to any particular purpose. It does not solve any particular problem, which can sometimes mean it solves several problems poorly.
Why choose good implementations over bad ones? Because there is a truth of the matter about what constitutes a solution to a problem. As such, programming is an effort to create explanatory knowledge, which is a continuation of an age-old cosmological endeavor: the project of understanding reality, which is unique to people. When there is a problem, meaning a conflict between two ideas, it tells us that at least one of them is false, because, in reality, there is no such conflict. Hard-to-vary programs are preferred because choosing one of countless variants without any functional advantage is irrational. Since a good solution to a problem has sufficient explanatory power to explain everything the conflicting theories do, it usually has a conserving as well as unifying character. But do hard-to-vary programs contain truth, or are they merely useful? The answer is the former, and I will examine the connection between programs and explanatory knowledge in more detail in chapter 3.
Good programs have the inte...