minimalism

minimalism

By Kevlin Henney

Overload, 9(45):, October 2001


The appeal to "omit needless words" is made by Strunk and White [ Strunk-2000 ] concerning written composition:

Vigorous writing is concise. A sentence should contain no unnecessary words, a paragraph no unnecessary sentences, for the same reason that a drawing should have no unnecessary lines and a machine no unnecessary parts. This requires not that the writer make all sentences short, or avoid all detail and treat subjects only in outline, but that every word tell.

Whilst we may muse about engineering, architectural or craft metaphors for software development, there is no denying that, in essence, programming is writing. It is a form of communication that has two distinct audiences: us and the machine. Although there are times when it might not feel this way, the machine is easily pleased, demanding little more than well-formed code. We, however, are a little more complex and discerning: we demand that our communication communicate.

As a discipline of composition much of what can be said for writing natural language is directly applicable to code. There is no virtue in long-windedness and, by way of balance, there is also no virtue in code that is unreadably terse. As ever the appeal is for well-written code. Code that is simple and clear, brief and direct.

motivation

Minimalism was the subject and title of my ACCU Spring Conference 2001 keynote [ Henney2001a ]. In reflection of the topic I dispensed with the usual parade of slides and detailed notes, making do with just two slides: the title and the agenda. As with an iceberg, this was just the tip: I personally had no shortage of material. Whether it was cool or not is another matter, but the feedback was good and I have had many requests for my notes or some written form on the topic. This column is a response, albeit a little delayed, to those requests.

We encounter creeping featurism in software on a regular enough basis, most tangibly in our role as application users: many options, many ways to set them, many ways to lose track of them, many cunningly hidden defaults - many subtleties. Sometimes we seem to be slaves to the application-fattening tedium of common but ineffective idioms.

My laptop, for example, is physically well designed: functional but appealing, lightweight but robust, minimal but complete. Not a trace of surplus anywhere, and certainly the best laptop I have had to date. But there's always something that lets the side down. At the time of the conference, the laptop was a very recent acquisition. I had only just set it up; there were still a few applications and personalisations outstanding when I went to give it its first public outing.

In performance it has been said that you should never work with animals and children. You can add computers to that short list. Part way through the keynote a dialog box cheerfully popped up to inform me that the batteries were now fully charged. Of course, being the presenter - with my back turned to the projection - I was in the privileged position of being the last to know. However, it did serve to illustrate a point: the subtle difference between a feature and something that is useful.

What an astonishingly useless notification to have set up by default. Why, in a logged-on existence already clogged with the dismissal of a seemingly never-ending procession of dialog boxes, would I want to be notified with any urgency that the batteries were full? Am I about to lose data? Has something crashed? Has a connection to a device or a network been established? Or dropped? Attention! Attention! Something not very significant has happened! ... OK? (The standard, but intent free, appeal gracing the lone button on such junk dialog boxes.) If you listen carefully enough, you can probably hear the late great Douglas Adams chuckling to himself in the background.

Well, yes, there is always the outside chance that I might find the information useful, but let's stop and think for a moment about how outside that chance is. Let's do some design. How is a laptop with a battery and power-saving features likely to be used? Typically either at a desk and plugged in or on the road and unplugged. In the latter case you are going to need to plug it in again at some point. If you are just going to be sitting at a desk all day, you really don't care when the battery fills up. Why would you? You're plugged into the mains concentrating on your work (or avoiding it, as the case might be): the battery doesn't matter. And if it does, that's why there's at least one helpful battery indicator in clear view: on the laptop itself or the (virtual) desktop. So there's already enough display information for both the bored casual user and the twitchy concerned power user.

If you're in a hurry and you need to pack up your bags and go, 97% is as good as a 100%. The 100% mark does not have the same (show-stopping) magic as 0%, where the difference between 3% and 0% really is important. After you've been hacking away on a train for a couple of hours and have just turned up to a meeting to give a presentation - in a room fitted with such modern gimmickry as mains electricity - it is entirely likely that the battery charge is of no interest whatsoever. As with over-helpful colleagues, it is more likely to be an irritating hindrance.

So, "battery full" notification is useful in perhaps a handful of situations and for a minority of people. As demonstrated through the exploration of usage scenarios, it will be the exception rather than the rule. It is certainly not a reasonable default. However, now that its discovery had been thrust upon me, I was strongly motivated to disable the default... but first I had to find it.

When I eventually found a moment for a get-to-know-your-laptop-better session, some rummaging around revealed that power management options were scattered across many different small applications, dialog boxes and menus. Some of the available options seemed to complement one another, some seemed to contradict one another, and some just wanted to tell you their copyright details - "OK"? That's the thing about features: if you don't watch out, they creep. Creep with all the coherence, supervision and elegance of most things on the planet that creep. And no, that's not "OK".

There is a moral to all this, and it's not "don't take new-born laptops to conferences" (it proves to be quite difficult to test out a conference situation in the comfort of your own home). Try to design for use rather than for features. As a guideline this advice is especially useful when applied to the ordinary and everyday, as opposed to the novel and groundbreaking. As an aside, there is a certain irony that the associated accessories web site for what is a highly usable laptop borders on the unusable. In spite of its gloss and featurism, plain hand-written HTML with simple CGI usage - think classic mid-1990s style - would have been a marked improvement on the song and dance on offer. It has been said that techies are not necessarily good web designers; let the same be said of marketing departments.

Features are important, but in the absence of unbounded development resources, something often has to be sacrificed on the altar of schedule. With the basic four variables you have to play with - scope, resources, quality and time - it is more reasonable to take the knife to features than either developers or quality, or indeed knocking over the altar. Understanding the model of usage is what informs this selection. So, given the choice, make specific common use simpler and more direct at the expense of generalised unproven utility. Design to be used. One consequence of making things more usable is that you work to identify and meet the need, thereby omitting the needless.

intent

My aim is not so much to explore software as executed as to examine software as written. Tales from the end user are useful and their lessons can be redirected.

At a previous keynote, I offered a definition of design that I have found useful [ Henney1999 ]: "Design is a creational and intentional act: conception and construction of a structure on purpose for a purpose". In other words not only is design an act carried out with intent - whether at the keyboard, on a whiteboard, on the back of an envelope or wherever - it is also about embodying the structure with intent. These are the two sides of the word intentional. Programming is about expression, which is about intent, which is about meaning. Hence clarity and brevity are significant.

Verbosity often represents a loss of intent, a loss of design information. I have found that many verbose coding practices are justified by claiming simplified debugging. It becomes the case that when programmers are so focused on debugging - what happens when things go wrong - that their attention is not focused on getting it right in the first place. It is often said that prevention is better than cure. No matter how laudable and well meant the cure is, choosing your practices is a matter of priority and perspective.

I recently read a list, from a GUI and software-development specialist, of top skills that should be required of developers: debugging was listed, but testing was not. I believe that this is symptomatic of the industry as a whole. Of course, debugging skills are not useless, but you will learn - and offer - a lot more from honing your unit-testing skills and understanding design by contract than you will sitting in a debugger.

I generally regard the use of this tool as a last resort; to some degree an admission of failure. This is not some kind of macho posturing ("real programmers don't need debuggers") but quite the opposite: there is an anti-intellectual machismo associated with reliance on stepping through code. Intellectualism is far from being a virtue in its own right, but, given that programming can be considered a matter of applied thought, revelling in anti-intellectualism seems somewhat inappropriate. I have heard the point of view supported by liberal use and abuse of the word practical. Let me put it to you this way: bugs are not practical; preventing them is more practical than finding them. I would suggest that the debugger exists to solve a smaller class of problems than it is normally used to solve.

For example, consider the following code fragment:

if(enabled == false)
{
  enabled = true;
}
else 
{
  enabled = false; 
}

and contrast it with:

enabled = !enabled;

This is not heavy number crunching code that requires a good understanding of advanced applied mathematics and numerical computing techniques. This is simple Boolean algebra, the bread and butter of programming: the toggling (or even, flipping) intent is clearer in the second and shorter fragment. It does not have redundant checks for equality against Boolean constants (for consistency, surely the result of such a check should in turn be compared against true, and then that result in turn...), expressing the idea of toggling directly and succinctly. If programmers cannot work at this level, why should companies entrust them with anything as important as development that will affect their assets and future revenue stream?

As programmers we learn to read through - by which I mean into more than I mean down - code to uncover the intent. We get so used to such reading that sometimes we forget to question alternatives, drawn in by habit to uncover the intent at perhaps a lower level than is strictly necessary. Consider loops. They are both the dynamo and motor of almost all programs, whether explicit or hidden. And oh how we love to write them:

std::vector<std::string> items;
... // populate items
for(std::size_t at = 0; at != items.size(); ++at)
  std::cout << items[at] << std::endl;

In C++, the received orthodoxy is that we should be writing in terms of iterators and not indices:

std::vector<std::string> items;
... // populate items
for(std::vector<std::string>::iterator
    at = items.begin();
    at != items.end(); ++at)
  std::cout << *at << std::endl;

Hmm, impressively less readable. Certainly orthodox, but the intent now seems buried deeper beneath the debris of syntax. When we "express coordinate ideas in similar form" [ Strunk-2000 ] we have the benefit of uniformity and ease of recognition. For instance, the loop logic remains unchanged should we change the container type. So why has this advice seemingly backfired? Pulling in std can make some difference, but not as much as you might hope. Perhaps it is a matter of avoiding some obvious repetition, factoring out commonality in the form of a better type name:

typedef std::vector<std::string> strings;
strings items;
... // populate items
for(strings::iterator at = items.begin();
    at != items.end(); ++at)
  std::cout << items[at] << std::endl;

Now, not only will the loop logic remain unaffected by a change in container, but the text of the loop will also remain unchanged:

typedef std::set<std::string> strings;
strings items;
... // populate items
for(strings::iterator at = items.begin(); at != items.end(); ++at)
  std::cout << *at << std::endl;

But we can do better. The main source of repetition is the source of repetition. Part of the point about C++'s STL and those nested iterator names is that it is not really our job to spell out all the types: let the compiler do it for you. Algorithm-based functions are templated to do just this. They encapsulate loops and iterator types. With the assistance of function objects, they can even make other type differences irrelevant. Decoupling through templates can be applied all the way through, making the code more general. Starting with the loop body:

class print {
public:
  print(std::ostream &out) : out(out) {}
  template<typename argument_type>
  void operator() (const argument_type &to_print) const
  {
    out << to_print <<  std::endl;
  }
private:
  std::ostream &out;
};

and then the loop:

std::vector<std::string> items;
... // populate items
std::for_each(items.begin(), items.end(), print(std::cout));

Something to notice: thanks to the idioms and names involved, the intent of the loop is clearer. In fact, the loop housekeeping is hidden. It's irrelevant; it does not clarify the intent. Of course, if you don't know STL, then what I have just said may not appear to hold water. That too is irrelevant. Why? I am writing it for an audience that can read STL. If the reader is not in that audience, then they are not in that audience. I am not writing it for C programmers, Visual Basic programmers, Java programmers or classic-C++ programmers. Code is not written to be read by everyman: it needs an audience, and will be better off for having one in mind.

Something else to notice: the code has been made more general and, in consequence, there is more up-front code to be written. Is this a good thing? If the problem had been to generalise stream output as a function object, it is a fine solution... but it wasn't. The challenge was to make the intent of the loop clearer. Scope drift and overgeneralisation are common problems. I happen to like the style of the print solution [Henney2001b], but that's the solution to a slightly different problem. Let's try again:

std::vector<std::string> items;
... // populate items
typedef std::ostream_iterator<std::string> out;
std::copy(items.begin(), items.end(), out(std::cout, "\n"));

Now the code is short, without any up-front extras, and is both quite specific and sufficiently general. This generality comes free with the standard abstractions we are using: we can work in terms of iterators independently of the container (or non-container) type. We didn't have to write any extra code to achieve this generalisation, so it is a bonus. The STL is general so that we can be specific. To the STL literate the code captures the intent clearly and crisply.

compression

This sentence no verb. The previous sentence is an example of compression. It could not have been made more direct by adding anything as its structure is intimately bound up in its meaning. In fact, its structure is its meaning. Taking time out to add things would only weaken it and miss the point entirely. This is compression; it is about intent, it is about sufficiency, and it is about time it was considered a valuable property of code. It is not a question of style versus substance: the style is the substance.

So what is the relationship between compression and that other popular code quality, abstraction? Well, first of all, what is abstraction? Jim Coplien's response - "Abstraction is evil" - is perhaps provocative, but it is certainly thought provoking [ Milne2001 ]. Jim Coplien can be found wielding this soundbite at many a conference. It is a reaction to the devaluation the word has suffered - abstraction has just become another word for good, just another hype term - and an invitation to consider compression in contrast to abstraction. In misuse, abstraction is often used as a hand wave to avoid necessary detail and specifics in the consideration of detailed code with a specific purpose. "Abstraction is evil" is also a soundbite, and all soundbites are abstractions - a self-referential irony not lost on Cope.

More conventional answers to the question include "selective ignorance" and "omission of unnecessary detail" which are, like the title of this article and Cope's slogan, fine soundbites, but are framed in the negative and are somewhat lacking in detail. Useful iconic forms that act as placeholders for deeper understanding, but on their own are essentially useless for motivating or informing actual development. Unlike technology, any advice that is insufficiently advanced is indistinguishable from magic.

Dijkstra offers perhaps the best motivation behind abstraction [ Dijkstra1972 ]:

In this connection it might be worthwhile to point out that the purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise.

Compare this with Dick Gabriel's description of compression [ Gabriel1996 ]:

Compression is the characteristic of a piece of text that the meaning of any part of it is "larger" than the piece has by itself. This is accomplished by the context being rich and each part of the text drawing on that context - each word draws part of its meaning from its surroundings. A familiar example outside programming is poetry whose heavily layered meanings can seem dense because of the multiple images it generates and the way each new image or phrase draws from several of the others.

Thus compression complements abstraction, but is not necessarily its opposite. Compression is often a result of appropriate identification and application of abstractions. In other words, abstraction considered as an artefact rather than as a process. For example, STL iterators and iterator ranges are abstractions. As we have seen, iterators can result in compression... or not. The difference comes from the "appropriate identification and application". Even good abstractions will not automatically bless your code with clarity and grace. We are back to intent and design: compression and abstraction are not just about leaving bits out, they are about expressing yourself with a directness, strength and clarity that could not necessarily be achieved by other means. They are about effect, but without drama or melodrama.

Consider an alternative abstraction to iterators for position: integers as indices. These work fine for array-like containers, such as std::vector or std::deque, but are quite inappropriate for linked lists or content-based containers, such as std::list and std::map. And fully beyond their grasp is the ability to represent access to non-containers meaningfully, such as iteration over streams.

As an example, the common task of counting words in a text file or stream can be reduced to a single statement of executable C++ code [ Henney2001c ] when built on the appropriate abstractions:

typedef std::istream_iterator<std::string> in;
std::cout << std::distance(in(std::cin), in());

Want to count characters instead?

typedef std::istreambuf_iterator<char> in;
std::cout << std::distance(in(std::cin), in());

or lines?

typedef std::istreambuf_iterator<char> in;
std::cout << std::count(in(std::cin), in(), '\n');

These fragments are all compact and fluffless, crisp and essential. STL iterators are not the only appropriate abstraction; different abstractions give rise to dissimilar constructs that can be similarly elegant and direct. For example, language support for closures - such as block objects and lambda functions found in Smalltalk, Lisp, Ruby, Python and others - gives rise to an alternative, inverted enumeration method style of iteration [ Beck1997 ]. With anonymous inner classes this style can be mimicked in Java, although not as cleanly and effectively as one might wish. In C++ the convenience of this alternative style is inaccessible, although such inversion of control flow can still have a role to play.

In all fields of communication we must recognise the importance of idiom. Compression appeals to common idiom (e.g. the use of the + operator to represent addition or concatenation) without resorting either to private language (e.g. #define your own 'keywords' and certain bizarre applications of operator overloading) or to lowest common denominator baby talk - don't write code for novices unless you are writing code for novices.

Operator overloading is a case in point: the fact that some people misuse it was the motivation for leaving it out of Java; the fact that it is also used to make code clearer and briefer was ignored. An example of solving the wrong problem that has disappointingly left Java with weaker powers of abstraction and compression for value-based concepts than either its immediate family, C++ and C#, or indeed scripting languages such as Ruby and Python. The effect of operator overloading, or the effect of its absence, is particularly noticeable when working with objects of numeric, date or currency type. Consider the merits of a + b - c versus a.add(b).subtract(c) .

I don't really want to focus on language design except where it significantly affects programmer expressiveness. It is worth pointing out that children generally understand the use of - for subtraction before they can spell subtraction. What happens to them between the years of early schooling and taking up a job as a programmer is anyone's guess, but experience suggests that their ability to comprehend arithmetic operators does not fall below their ability to spell. The utility of operator overloading has been recognised and perhaps this convenience will be added to Java in the future [ Gosling ].

On inappropriate use of overloading there is also a lot you can say. To counterbalance the elegance and minimalism of the STL one has to look no further in C++ than std::auto_ptr. A triumph of subtlety over expectation, of obscurity over clarity. Ordinary copying semantics make no sense for auto_ptr , so what look like copying operations - the copy constructor and the copy assignment operator - have been overloaded misleadingly as move operations. Fundamentally different semantics dressed in sheep's clothing. For the problem being solved, this has led to a ridiculously complex and narrow design that has proven to be a hotspot for specification errors. However, auto_ptr alone does not build a sufficient case for rejection of overloading in any of its forms. It instead emphasises the necessity of respect for idiom: the distinction between feature and usage, mechanism and method.

Compression is neither an invitation to write WORN code (Write Once, Read Never) nor an incitement to write tightly coupled goo. Quite the opposite. The density arises from necessity, sufficiency and expression. The result is a certain brevity, which "is a by-product of vigour" [ Strunk-2000 ], the result of making it stronger. Brevity is categorically not the same as obfuscation [ Henney1995 ]: it should be no shorter than is strictly necessary. If it looks like line noise - and you're familiar with the language - then it's too short.

references

[Beck1997] Kent Beck, Smalltalk Best Practice Patterns, Prentice Hall, 1997.

[Dijkstra1972] Edsger W Dijkstra, "The Humble Programmer", Communications of the ACM 15(10), October 1972.

[Gabriel1996] Richard P Gabriel, "Patterns of Software: Tales from the Software Community", Oxford, 1996.

[Gosling] James Gosling, "The Evolution of Numerical Computing in Java", http://java.sun.com/people/jag/FP.html .

[Henney1995] Kevlin Henney, "The Elements of Style: Brevity is the Soul of Wit", CVu 7(2), January 1995, also available from http://www.curbralan.com .

[Henney1999] Kevlin Henney, "Design: Concepts and Practices", JaCC, September 1999, also available from http://www.curbralan.com .

[Henney2001a] Kevlin Henney, "Minimalism", ACCU Spring Conference 2001, March 2001.

[Henney2001b] Kevlin Henney, "The Miseducation of C++", Application Development Advisor 5(3), April 2001, also available from http://www.curbralan.com .

[Henney2001c] Kevlin Henney, "If I Had a Hammer...", Application Development Advisor 5(6), July-August 2001, also available from http://www.curbralan.com .

[Milne2001] Ewan Milne, "A Touch of Abstraction", CVu 13(1), February 2001.

[Strunk-2000] William Strunk Jr and E B White, The Elements of Style, Fourth edition, 2000.






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.