Code is not the only sort of thing with an optimal chunk size. Languages and APIs (such as sets of library or system calls) run up against the same sorts of human cognitive constraints that produce Hatton's U-curve.
Accordingly, there are two properties that Unix programmers have learned to think very hard about when designing APIs, command sets, protocols, and other ways to make computers do tricks. These are compactness and orthogonality.
Compactness is the property that a design can fit inside a human being's head. A good practical test for compactness is this: does an experienced user normally need a manual? If not, then the design (or at least the subset of it that covers normal use) is compact.
Compact software tools have all the virtues of physical tools that fit well in the hand. They feel pleasant to use, they don't obtrude themselves between your mind and your work, they make you more productive — and they are much less likely than unwieldy tools to turn in your hand and injure you.
Compact is not equivalent to ‘weak’. A design can have a great deal of power and flexibility and still be compact if it is built on abstractions that are easy to think about and fit together well. Nor is compact equivalent to ‘easily learned’; some compact designs are quite difficult to understand until you have mastered an underlying conceptual model that is tricky, at which point your view of the world changes and compact becomes simple.
Very few software designs are compact in an absolute sense, but many are compact in a slightly looser sense of the term. They have a compact working set, a subset of capabilities that suffices for 85% or more of what expert users normally do with them. Practically speaking, such designs normally need a reference card or cheat sheet but not a manual.
The concept is perhaps best illustrated by examples. The Unix system call API is compact, but the standard C library is not. While Unix programmers easily keep a subset of the system calls sufficient for most applications programming (filesystem operations, signals, and process control) in their heads, the C library on modern Unixes includes many hundreds of entry points for, e.g. mathematical functions, that won't all fit inside a single programmer's cranium.
Among Unix tools, make(1) is compact; autoconf(1) and automake(1) are not. Among markup languages, HTML is compact, but DocBook (a documentation markup language we shall discuss in Chapter 16 (Documentation)) is not. Man-page macros are compact, but troff(1) markup is not.
Among general-purpose programming languages, C and Python are compact; C++, Perl, Java, Emacs Lisp, and shell are not (especially since serious shell programming requires you to know half-a dozen other tools like sed(1) and awk(1)).
Some designs that are not compact have enough internal redundancy of features that individual programmers end up carving out compact dialects sufficient for that 85% of common tasks by choosing a working subset of the language. Perl is like this, for example. Such designs have a built-in trap; when two programmers try to communicate about a project, they may find that differences in their working subsets are a significant barrier to understanding and modifying the code.
Non-compact designs are not automatically doomed or bad, however. Some problem domains are simply too complex for a compact design to span them. Sometimes it's necessary to trade away compactness for some other virtue, like raw power and range. Troff markup is a good example of this. So is the BSD sockets API. The purpose of emphasizing compactness as a virtue is not to teach the reader to treat compactness as an absolute requirement, but to do what Unix programmers do — value compactness properly, design for it whenever possible, and not throw it away casually.
Orthogonality is one of the most important properties that can help make even complex designs compact. In a purely orthogonal design, operations do not have side effects; each action (whether it's an API call, a macro invocation, or a language operation) changes just one thing without affecting others. There is one and only one way to change each property of whatever system you are controlling.
Your radio has orthogonal controls. You can change the station it's tuned into independently of the volume level, and (if the radio has one) the stereo balance control will be independent of both. Imagine how much more difficult it would be to use a radio on which the volume knob affected the tuning — you'd have to compensate by tweaking the tuning control every time after you changed the volume. Worse, imagine if the tuning control also affected the volume; then, you'd have to adjust both knobs simultaneously in exactly the right way to change either volume or tuning frequency alone while holding the other constant.
Far too many software designs are non-orthogonal. One common class of design mistake, for example, occurs in code that reads and parses data from one (source) format to another (target) format. A designer who thinks of the source format as always being stoed in a disk file may write the conversion function to open and read from a named file. Usually the input could just as well have been any file handle. If the conversion routine were designed orthogonally, e.g. without the side-effect of opening a file, it could save work later when the conversion has to be done on a data stream supplied from standard input or any other source.
Doug McIlroy's advice to “Do one thing well” is usually interpreted as being about simplicity. But it's also, implicitly and at least as importantly, about orthogonality.
The problem with non-orthogonality is that side-effects complicate a programmer's or user's mental model, and beg to be forgotten — with results ranging from inconvenient to dire. When you do not forget them, you're often forced to do extra work to suppress them or work around them.
There is an excellent discussion of orthogonality and how to achieve it in The Pragmatic Programmer [Hunt&Thomas]. As they point out, orthogonality reduces test and development time, because it's easier to verify code that neither causes side-effects nor is dependent on side effects from other code — there are fewer combinations to test. If it breaks, orthogonal code is more easily replaced without disturbing the rest of the system. Finally, orthogonal code is easier to document and re-use.
The basic Unix APIs were designed for orthogonality with imperfect but considerable success. We take for granted being able to open a file for write access without exclusive-locking it for write, for example; not all operating systems are so graceful. Old-style (System III) signals were non-orthogonal, because signal receipt had the side-effect of resetting the signal handler to the default die-on-receipt. There are large non-orthogonal patches like the BSD sockets API and very large ones like the X windows drawing libraries.
But on the whole the Unix API is a good example — otherwise it not only would not but could not be so widely imitated by C libraries on other operating systems. This is also a reason that the Unix API repays study even if you are not a Unix programmer; it has lessons about orthogonality to teach.
The Pragmatic Programmer articulates a rule for one particular kind of orthogonality that is especially important. The “DRY Rule” is: every piece of knowledge must have a single, unambiguous, authoritative representation within a system. The DRY in the name of the rule stands for a shorter and pithier way of putting this: Don't Repeat Yourself!
Repetition leads to inconsistency and code that is subtly broken, because you changed only some repetitions when you needed to change all of them. Often, it also means that you haven't properly thought through the organization of your code.
Constants, tables, and metadata should be declared and initialized once and imported elsewhere. Any time you see duplicate code, that's a danger sign.
Often it's possible to remove code duplication by refactoring — changing the organization of your code without changing the core algorithms. Data duplication sometimes appears to be forced on you. But Hunt & Thomas suggest some valuable questions to ask:
If you have duplicated data used in your code because it has to have two different representations in two different places, can you write a function, tool or code generator to make one representation from the other, or both from a common source?
If your documentation duplicates knowledge in your code, is there a way you can generate parts of the documentation from parts of the code, or vice-versa, or both from a common higher-level representation?
If your header files and interface declarations duplicate knowledge in your implementation code, is there a way you can generate the header files and interface declarations from the code?
From deeper within the Unix tradition, we can add some of our own:
Are you duplicating data because you're caching intermediate results of some computation or lookup? Consider carefully whether this is premature optimization; stale caches (and the layers of code needed to keep caches synchronized) are a fertile source of bugs.
If you see lots of duplicative boilerplate code, is there a way to generate all of it from a single higher-level representation, twiddling a few knobs to generate the different cases?
The reader should begin to see a pattern emerging here...
In the Unix world, the DRY Rule as a unifying idea has seldom been explicit — but heavy use of code generators to implement particular kinds of DRY are very much part of the tradition. We'll survey these techniques in Chapter 9 (Generation).
We began this book with a reference to Zen: “a special transmission, outside the scriptures”. This was not mere exoticism for stylistic effect; the core concepts of Unix have always had a spare, Zen-like simplicity that continues to shine through the layers of historical accidents that have accreted around them. This quality is reflected in the cornerstone documents of Unix, like The C Programming Language [K&R] and the 1974 CACM paper that introduced Unix to the world; one of the famous quotes from that paper observes “...constraint has encouraged not only economy, but also a certain elegance of design”. That simplicity came from trying to think not about how much a language or operating system could do, but of how little it could do — not by carrying assumptions but by starting from zero.
To design for compactness and orthogonality, start from zero. Zen Buddhism teaches that attachment leads to suffering; experience with software design teaches that attachment to unnoticed assumptions leads to non-orthogonality, non-compact designs, and projects that fail or become maintenance nightmares.
To achieve enlightenment and surcease from suffering, Zen teaches detachment. The Unix tradition teaches the value of detachment from the particular, accidental conditions under which a design problem was posed. Abstract. Simplify. Generalize. Because we write software to solve problems, we cannot completely detach from the problems — but it is well worth the mental effort to see how many assumptions you can throw away, and whether the design becomes more compact and orthogonal as you do that. Possibilities for code reuse often result.
Jokes about the relationship between Unix and Zen are a live part of the Unix tradition as well. This is not an accident.