Today I am pleased to announce another guest post by Matt Bentley. Matt writes about when elegance in code can hurt simplicity. If you’d like to write a guest post or co-authored post yourself, this is for you – get in touch!
Nobody’s formally defined elegance in programming, so here goes: for the purposes of this document, ‘elegance’ is defined as ‘expressing the semantic intent of code in such a way that it is both terse yet also immediately recognisable as to what the semantics are’.
An example of this in C++ is the “for” loop, which has gradually evolved from:
for (std::vector::iterator it = vec.begin(); it != vec.end(); ++it)
for (auto it = vec.begin(); it != vec.end(); ++it)
for (auto i : vec)
The problem with elegance in programming is there is an inherent tension between elegance and simplicity. Simplicity is created by having fewer things to memorize, fewer building blocks and components. The fundamental building blocks used to create the initial
for loop above are fairly straightforward – each token can be understood individually without inference because they are part of the greater structure of the language. With auto, we remove a level of obviousness, exchanging it for brevity, and thus require the programmer to infer the type whilst reading. In the third instance of the
for loop, additional understanding is now required by the programmer yet again, as it is now a domain-specific syntax which has no building blocks (aside from
for) common with the rest of the C++ language. But once the syntax is understood, it becomes easier to read.
This is not problematic in-and-of-itself – elegance should certainly be a Consideration when designing languages. But it becomes problematic when there’s an overweening focus on it, as elegance inherently leads toward domain-specific code. The most elegant solution will almost always be something which the building blocks of the language do not allow for because their expression is clumsy or verbose by comparison with a more succinct custom expression for a given scenario. But when one develops too many of these scenarios, it becomes a lot for any newcomer to take on board.
And such is the fate of modern C++. Over the past twenty years, it feels like C++ has shifted as far as is humanly possible, away from a building-block approach to coding (easy to understand), to a series of ‘elegant’ and ‘comprehensive’ approaches – where there is something specific for every case, and it is considered desirable to cater for every scenario in the most elegant fashion. The downside of this is that the wide morass of scenarios, idioms, and caveats necessary to do this are impossible to learn in their entirety. More than one eldritch C++ programmer has commented that nobody fully understands C++ anymore. Its understanding is now a hive-mind approach, resulting in a gradiated learning curve reaching an exponential point, only to be attained by the heaped corpses of a thousand burned-out programmers.
Take C by comparison – now now, I’m not going to enter a flame war, I’m just using it as an example (hush children hush!).
The building blocks are there. It is arguably as simple as it can be made without being behind the times. But it is much faster to comprehend – less expressive perhaps, less quick to write code in once understood, maybe. But the building blocks are fewer, and the edge case scenarios as introduced by needs for elegance, far fewer. Look at vector insert in C++:
iterator insert (const_iterator position, const value_type& val);
iterator insert (const_iterator position, size_type n, const value_type& val);
iterator insert (const_iterator position, InputIterator first, InputIterator last);
iterator insert (const_iterator position, value_type&& val);
iterator insert (const_iterator position, initializer_list il);
Does it really feel necessary to include five different types of insert? Yes? Why. Three would be enough – the range and initializer list ones can be replicated by the programmer with simple loops. More work for the programmer? Yes. Fewer things to memorize for the programmer and community at large? Yes. Perhaps not the strongest example, but it demonstrates a fundamental tradeoff. Every time you try and make a language more comprehensive, more elegant, more all-things-to-all-people-and-no-waste, you raise the learning bar. You additionally raise the comprehension bar for when re-reading code. Take a relatively simple language like C, however, and though there may be a lot of wasted effort in the coding process, due to the fewer amount of building blocks, the end result is code that’s simpler to write, simpler to read, and faster to learn.
Compare it to old lego vs new lego: sure, the new stuff looks smoother, but it’s all custom-builds with no imagination required. You can’t just build from scratch – you have to read the manual. At the risk of repeating myself: every time you think something can be done more expressively, more elegantly, more comprehensively – actually Think. Do we need this? Is the tradeoff between doing it with existing building blocks really worth it? Is it worth a newcomer programmer’s time and energy to learn this additional thing? If not, think back to when you were young, back to when you were first learning programming; would you want to learn the large behemoth that C++ has become? Or would you want to stay sane?
… having said all that,
std::enable_if really sucks. Roll on concepts. Just sayin’.