Code as communication: mental models & expectation management

John Ohno on 2018-12-24

Communication is a useful lens for thinking about programming, because it clarifies certain things about good style left unaddressed by competing models (like programming-as-instruction or programming-as-engineering). Programming is a unique kind of communication, though.

A program has two distinct audiences: human beings (who are expected to understand the intent of the code, its actual behavior, and how the two are related) and computers (who are expected to execute the instructions in a consistent manner). The most important constraint a programmer must optimize for is to ensure that the intended behavior, a human’s expectation of actual behavior, and the actual behavior as interpreted by any computer, are as close to identical as possible. This is a bit unusual: many forms of communication are targeted at a single audience (or a contiguous range of people), and those that are intended for disjoint audiences are often intended to mean something different to one than do the other (like a dogwhistle). The only kinds of code intended to have a hermenuetic double-meaning are malware and joke projects (like entries into the Obfuscated C contest).

The second most important constraint is that the effort for a human being to come to a reasonable hypothesis about expected behavior should be minimized. (In this way, maintenance is made easy.) This is also unusual: stories and especially jokes derive their power from misdirection and from hidden depths. Dramatic irony is a code smell: if a piece of code appears to have a plan the reader knows is doomed by outside circumstances, odds are that this is not intentional on the part of the author. Whatever hardship it goes through, a program is supposed to have a happy ending, not a tragic one.

The kind of literature that code has the most in common with, based on these two metrics, is the textbook. Textbooks can be (and often are) amusing and entertaining, but the primary purpose of a textbook is to gradually synchronize the knowledge of different readers from different backgrounds with some unified baseline.

Code is written in a formal language, in order to make consistent computer interpretation possible. Formal languages can be designed in different ways, with different side effects on how it is read.

Few people have complete and perfect knowledge of even their preferred programming language. In order for a reader of code to become confident about and correct in their expectations about the behavior of that code, they must be relatively certain about their interpretation of the behavior of language constructs.

One way to do this is to use idioms and patterns: large-scale structures that are recognizable and, if implemented correctly, predictable. (A failure mode of this: an incorrectly implemented idiom or pattern is misleading, increasing a reader’s confidence about an incorrect model.) This method is common in conventional ‘general purpose’ languages, like C++, Java, and Python.

Another way to do this is to use a small set of powerful constructs — operations that have clearly-defined but highly general behaviors, wherein specific behaviors can be quickly understood as interactions between a small number of these general operations. (A failure mode of this: an incorrectly understood behavior at one point quickly infects the rest of the program, because there is little redundancy.) This method is common in minimalist languages like forth, scheme, and APL, and in functional languages like haskell.

A third, middle way: while maintaining highly specific behaviors, find conceptual and structural similarities between these behaviors and ensure that those similarities are indicated by consistent naming & consistent behaviors. For instance, all ‘collection’-like objects should have the same syntax for retrieving an item or counting the number of items, maps (being an association between some object & some other object) and arrays (being an association between an integer & some object) should behave as similarly as possible, and there should be no special/magic corner case types or operations that are only capable of being used by foreign or built-in code. (Failure mode: language designers cheat a little bit when it’s convenient, or forget previously established patterns, or use variations in previously established patterns to encode information useful to the compiler but not useful to a programmer, or fail to notice similarities that are obvious to people who use the language.) Lua and Python largely succeed at this, while C++, Java, and Perl largely fail.

Like textbook authors, we should choose our terminology to be minimally confusing, and redefine terms when they might be ambiguous. We should try to avoid using terminology in ways at odds with its general use, in case someone does not read or does not understand our new definitions, but we should not be afraid to coin our own terms in cases where the existing ones are so loaded with contradictory ideas for our intended audience that using them would be more confusing than starting with a fresh slate. Teaching readers existing terminology is secondary to teaching readers concepts, and alternative terminology can be attached to those concepts once they are understood. In the case of programming, the concepts we are teaching are the structures, idioms, and procedures necessary to understand the operation of our program, & how that operation relates to the intent of the program.

A third concern of programming is reusability — typically in terms of modularization. A reader should know how to comfortably take pieces from a good program and reuse them in isolation. In other words, categorization and chunking acts not only as a learning aid (as it does in textbooks) but as a means of making tools and ideas directly accessible outside of their regular context. This means that modules should not only be optimized for intellectual similarity, but also for shared use.

Without the use of idiom & attempts to make evident the similarities between a new piece of code & other code already familiar to the reader, modularization chafes against these other constraints: we are trying to break code into small chunks, but this may expand the size of the code & the number of elements involved in analyzing behavior. (This is a common failure mode of readability in large Java and C++ projects: many small chunks, each doing very little, all of which need to be understood individually.) If we take advantage of idiom, however, our new chunks are both easily understood & easily remembered; if they take advantage of simple-yet-powerful constructs, then they also remain general (and so, since they aren’t very specific, there can be fewer of them).

Because of the use of these facilities to make modules both understandable and general, a well-designed project is small: it contains a small number of relatively-general operations built out of even-more-general operations (and these, if sufficiently general, may migrate into an even more general library), and because of the power of these relatively-general operations, those pieces that must be specific are kept small, in one place, and easily understandable. Distinctions-without-a-difference are noise, & if they are eliminated from a project, it becomes both smaller and more readable.

Code as instruction may optimize for performance, and code as engineering may optimize for stability, but code as communication optimizes for the clarity necessary to produce both performance and stability.