Attractiveness matters. Adam Tornhill uses a Clojure example to show how generalisation can be more beautiful than special cases.
The challenge of all software design is to control complexity. Less complexity means that our programs are easier to understand, reason about and evolve. This article shows how we can use beauty as a mental tool for that purpose. Starting in the field of the psychology of attractiveness, we’ll expand its theories on physical beauty to also cover code. In the process we’ll learn a bit about Clojure, meta-programming and destructuring.
The scandal
Beauty is a controversial subject. Research has shown that, everything else equal, a beautiful waitress gets more tips than a less attractive colleague. The politician with the better looks gets more votes than more ordinary opponents. Not even our judicial systems are immune to the power of beauty since offenders with the right type of facial features receive milder judgements. Beauty is indeed scandalous. And it’s a scandal backed by solid science [ Renz06 ].
Attractiveness is important to us. Often more important than we’d like to admit or even are aware of at a conscious level. So before we dive into code we’ll make a short detour into attractiveness psychology.
Beauty is average
At the end of the 80s, scientist Judith Langlois performed an interesting experiment [ Langlois90 ]. Aided by computers, she developed composite pictures by morphing photos of individual faces. As she tested the attractiveness of different pictures on a group, the results turned out to be both controversial and fascinating. Graded on physical attractiveness the composite pictures won. And they won big.
The idea of beauty as averageness seems counterintuitive. In our field of programming, I’d be surprised if the average enterprise codebase would receive praise for its astonishing beauty. But beauty is not average in the sense of ordinary, common or typical. Rather, beauty lies in the mathematical sense of averageness found in the composite faces.
The reason the composite pictures won is that individual imperfections get evened out with each additional morphed photo. As such beauty is more of a negative concept defined by what’s absent rather than what’s there. As the theory goes, it’s a preference shaped by evolution to guide us away from bad genes towards good ones.
Beautiful code
Translated to our world of software the good genes theory means consistency. Beautiful code has a consistent level of expression free from special cases. Just as deviations from the mathematical averageness makes a face less attractive, so does any coding construct that deviates from the main flow. Classic examples of special cases include conditional logic, explicit looping constructs or differing programming models for sequential vs concurrent code.
These constructs all signal bad genes in our programs. With beauty as a design principle we steer away from such special cases towards solutions that are easier to understand and grow.
A case study in named arguments
To classify code as beautiful, there has to be some tension in the design. Perhaps that tension comes from an alternative solution that, while initially tempting, would turn out less than optimal. Frequently, beauty in code arise from a hard problem whose solution is made to look easy and apparent.
My favourite example comes from Clojure. Clojure has grown at a rapid rate, but without losing track of its core tenets of simplicity and programmer power. While Clojure included novel concurrency constructs and strong meta-programming support from its very start, a more fundamental feature was missing: named arguments.
Named arguments are familiar to programmers in languages like Python and C#. The idea is that instead of relying on the positional placement of function arguments, the programmer specifies the name of the parameters. Based on the names, the compiler binds the objects to the right argument.
Say we have a C# method to fire at objects:
void Fire(Location at, Weapon weapon) { // implementation hidden as a // safety precaution }
Thanks to named arguments, a client can invoke this method in any of the following three ways:
-
Positional:
Fire(asteroid.LastObservation, ionCannon);
-
Named arguments, same order:
Fire(at: asteroid.LastObservation, weapon: ionCannon);
-
Named arguments, arbitrary order:
Fire(weapon: ionCannon, at: asteroid.LastObservation);
Named arguments serve to make the code on the call site more expressive. They also allow us to write intention revealing code by highlighting the core concept in the current context; if we write code for firing a weapon, prefer alternative #3. If we instead focus on shooting down asteroids, alternative #2 is a better fit.
The fire method is straightforward to translate into Clojure. We use
defn
to define a named function:
(defn fire [at-location, weapon] ; the implementation goes here... )
On the call site, we fire at the location of asteroids as:
(fire asteroid-location ion-cannon)
In older versions of Clojure that was it. I couldn’t name my arguments. But Clojure is a member of the Lisp family of programming languages and shares their characteristic of code as data. With that concept there’s no limit to what we can do with the resulting meta-programming facilities. Lack a feature? Well, go ahead and implement it. In Clojure the programmer has the power of the language designer.
This aspect of Clojure is possible since Clojure code is expressed using its own data structures. Consider the code snippet above. The parenthesis around the
fire
function delimits a list. The square brackets represent a vector of two elements (the function arguments
at-location
and
weapon
).
Besides bending your mind in interesting ways, meta-programming allows the developer to hook into the compiler and write code that generates code. From a pragmatic perspective, it means that Clojure can grow without any changes to the core language itself. This is a radical difference to popular languages in use today.
Armed with meta-programming support, the early Clojure community developed an extension to the language:
defnk
. It was intended to complement Clojure’s standard
defn
form to define functions. In addition, it allowed clients to use named arguments similar to the C# code above.
Having two different constructs to express the same basic thing affects our reasoning abilities. The largest cost is not the extra learning per se. The price we pay is during code reading. Suddenly we have one more concept to keep in our head. There’s nothing attractive about that.
Towards beauty through destructuring
defnk
never made it into Clojure. Instead Rich Hickey, Clojure’s creator,made an interesting design choice. He decided to extend the usage of an existing feature to cover named arguments too. That feature was destructuring.
Destructuring is a distant relative to pattern matching as found in several functional programming languages (see for example: Fogus & Houser [ Fogus11 ]). The feature makes it more concise to pull apart composite data structures into individual parts. Let’s pretend for a while that C# supported destructuring. Listing 1 is how it could look.
var incrediblyImportantNumbers = new Dictionary<string, double> { {“PI”, 3.14}, {“GoldenRatio”, 1.6180339887}, {“RubiksCubeCombos”, 43252003274489856000.0} }; // Now, pretend we have got destructuring to // pull apart the Dictionary: var {PI, GoldenRatio, RubiksCubeCombos} = incrediblyImportantNumbers; Console.WriteLine(PI + “, “ + “, “ + GoldenRatio + “, “ + RubiksCubeCombos); // Would print: 3.14, 1.6180339887, // 43252003274489856000.0 |
Listing 1 |
Identified by the name of the keys, destructuring pulls out the associated values into the provided variables. At this point you’re probably relieved to learn that I don’t have any involvement in C#’s evolution. But let’s leave my language design skills aside and translate the code to Clojure (see Listing 2). It’s similar to my fantasy code in Listing 1. It also has the unmistakable advantage of being valid code.
; Define a map of key-value pairs ; (aka dictionary): (def incredibly-important-numbers {:PI 3.14 :golden-ratio 1.6180339887 :rubiks-cube-combos 43252003274489856000}) ; Destructure the map into symbols, one for ; each key. ; First the destructuring pattern... (let [{:keys [PI golden-ratio rubiks-cube-combos]} ; ..then the dictionary to destructure: incredibly-important-numbers] (println PI ", " golden-ratio ", " rubiks-cube-combos)) ; Will print: 3.14, 1.6180339887, ; 43252003274489856000 |
Listing 2 |
By extending destructuring to function parameters, our fire function using plain
defn
would turn to this:
(defn fire ; destructuring pattern on function arguments: [& {:keys [at-location weapon]}] ; the implementation goes here...
The destructuring patterns form a mini language inside Clojure. With the extended scope of that mini language, Clojure programmers got the power of named arguments without the need to learn a new language construct. Better yet, we could fire weapons as:
(fire :weapon ion-cannon :at-location asteroid-location)
Similar to the process behind the attractive composite pictures this design evened-out individual differences between use cases. This has several benefits. Since we got rid of multiple ways to define functions, the resulting solution maintains conceptual integrity. From a programmer perspective we can re-use our mastery of how destructuring works and apply that knowledge to function argument lists too. This is a big advantage since there are fewer concepts to keep in our head.
Outro
Every design choice implies a trade-off. In beauty we have a mental tool that guides us towards evolutionary fit programs. As this short Clojure story illustrates, to generalize an existing concept is a more attractive path than to introduce a special case. It’s a technique we can use to drive us towards consistency independent of the programming language we work in.
Beautiful code looks obvious in retrospect but may well require hard mental work up front. It’s an investment that pays-off since code is read more frequently than it’s written. The decisions we make in our programs today exert their influence over time. Guided by beauty we pave an attractive way for future maintainers.
Thanks
Thanks for Björn Granvik, Mattias Larsson, Tomas Malmsten and the Diversify/Webstep team for valuable feedback! Also thanks to the Overload editorial team for their corrections!
References
[Fogus11] Fogus, M., & Houser, C. (2011) The Joy of Clojure . Manning.
[Langlois90] Langlois, J. H., & Roggman, L. A. (1990) Attractive faces are only average .
[Renz06] Renz, U. (2006). Schönheit . Berliner Taschenbuch Verlag.