34 comments

[ 4.8 ms ] story [ 85.5 ms ] thread
I find this kind of exhortation to be useless in practice, because while everyone agrees that code shouldn't be too clever, no-one agrees on what kind of code is being too clever.

For example, I'd argue that using C/C++ at all is probably "too clever" - humans are good at simulating the happy path, but very bad at simulating all the possible different ways such code might actually be executed. But all too often people are seen as smart and rewarded for "using such an efficient low-level language".

Conversely, I'd argue that exposing a business constraint as an algebraic datatype that forces you to use it correctly reduces the cleverness required to understand the code.

But I'm sure there'll be someone here who thinks the exact opposite, and bringing "cleverness" into the conversation doesn't actually bring us any closer to agreement.

Yeah, I agree, it's the old complex/complicated conundrum. What's easier: a super compact representation of the problem that uses high-level concepts, or a verbose listing of dumb procedural code? Chances are it depends on both who you ask and the problem at hand.
> Chances are it depends on both who you ask and the problem at hand.

100% this. Code runs on people but people differ dramatically. A more compact solution that leans on some more advanced language features might be totally acceptable in a team of people fluent with the language. The same problem in a team of generalists working across different tech stacks day-to-day might benefit from a more verbose solution.

As with most things in software it depends on context.

Be nice if there was a convenient way to have both.
I once read a HN comment the made an analogy to reading levels in a newspaper or other natural language. I found this very apt.

If you have a rich vocabulary and so does your audience, you can explain an idea succinctly. Imagine a chain of function compositions in one line, very declarative. You grasp it near immediately or else its very difficult.

If you don't, you use simpler words and need more of them. Imagine a few nested for loops in something like Go that you need to execute in your head to realize what's going on, but anyone can figure it out given a little time.

People frequently talk past each other on this topic. It can be very frustrating.

There's something I always thought about but haven't been able to express clearly, and that's about "shared mental models". For example, with Go, the standard library is huge, people tend to not use many libraries, especially ones that change the language, and most people write code the same way. That means that for any two people writing Go code, they share a big part of their mental model about what code should be. That makes it "boring" when one reads the other.

On the opposite end of the spectrum, you could put Scala. There are already a few ways to use Scala as a language: Java with better types, OO all the way, functional all the way. And then there are ecosystems. Zio, cats, Akka. All of that means that for two people writing Scala code, the shared mental model is going to be small unless they use it the exact same way.

You can infer some things from that. Languages with big standard libraries that are somewhat good will allow people to share a bigger part of their mental model compared to languages with bad standard libraries (people will create replacements) or languages with very small standard libraries. Languages with a lot of features that are actually used will have a low amount of shared mental models, but also languages with not many features as people will reimplement things themselves.

I'd argue the opposite really, re: 'using C/C++ at all is probably "too clever"'. By using modern C++ features it is possible to enforce a Rust-like model of ownership, without any memory management (eg. we don't use 'new' or 'delete' anywhere in our codebase). This helps reduce what the author calls "complicated, Klein-bottle-wannabe tricks", labyrinthine Java classes of GC goodness where you don't know where the code begins/ends.
> By using modern C++ features it is possible to enforce a Rust-like model of ownership, without any memory management

C++ fans always claim this is possible, but can never say how to determine whether any given codebase follows their rules, or give examples of e.g. popular open-source libraries that follow that approach. So I've stopped believing in it, personally.

> This helps reduce what the author calls "complicated, Klein-bottle-wannabe tricks", labyrinthine Java classes of GC goodness where you don't know where the code begins/ends.

I find that claim extremely dubious. The problems of such code are very rarely due to not having clear directions on any local ownership relationship, and lifetimes are not actually visible in C++ in any case.

I think “too clever” often equates to “too short.” There is real intellectual pleasure in figuring out how to express an algorithm in as few lines as possible. I personally love doing this. And, usually, shorter code is better code. But I have seen many cases where people take this too far. People reduce their code so much that it becomes incomprehensible. (No doubt I have been guilty of this.) So to your question of what is “too clever” I would say that I would prefer 10 lines of readable code to 1 line of unreadable code.
"We're not a development shop"

OK, so I've inherited a Single File App with multi-screen a multi screen main() function. I can tell what it's doing at a glance, except that there is a bug report, and something may be wrong.

First instinct "make the problem smaller". But how?

People who put effort into avoiding effort frequently succeed in moving effort.

If building something new, there are good alternatives to C/C++ that end up being more manageable for the human writing it.

That said, if you're stuck with C/C++, there are still many grades of complexity within that that can be tackled. Write boring code, instead of writing something complicated to save a few lines of code.

Actually, I am a big fan of C++ code. Smart code may cause problems and I agree with that. You know what else causes problems? Tool churn and language churn. At any time of the night and day I very much prefer figuring out somebody elses smart code than figuring out yet another tool that we apparently need. It may not necessarily be that one thing wastes more time than the other but I am 100% sure where my preference is as far as what kind of problem I like to work with.
Doesn't C++ have a lot of churn recently? The transition to C++11, best practices evolving, new features in C++14/17/20?
C++ is notorious for each new version introducing a new way of doing things, so I don't think it's a good anti-churn language (plain C is more defensible for that, though I wouldn't use it for other reasons). And there's certainly tool churn in the C++ world. And frankly C++ has been a poor choice for long enough that I don't think "churn" is a good reason to keep using it - I'm not exactly thrilled when I pick up a TCL codebase from 1996, but you can do it and it'll work.
I've run into the opposite issue. I like macro lists in C/C++ (not sure what people call them). It's the idea that you create a pre-processor list and then use that list to effectively code generate. For example you can use the list to generate enums, enum to string tables, case statements for all cases, etc.... It's clever but it's also IMO best practice because it prevents bugs. The reason they're used is it prevents the various parallel tables from getting out of sync. It's also arguably better than some external code generator because there are less dependencies.

So, is that a "clever" solution that should be avoided because it's "clever" or is it a best practice because it actually solves real problems? Problems which have cost me hours of debugging that could have been avoided if I'd just used the "clever" solution. For me it's the latter.

In fact, speaking to the OP, it also encodes the original programmer's logic since without them, the new programmer has to know the 4 to 9 places they'd need to edit to add to the list where as with them they only need to know 1.

it's a workaround for a lacking type system
Not really. It'd be more accurate to say it's a workaround for an existing proper hygienic macro system built on static introspection and code generation.
Not really. It's not a workaround - it just works.

Maybe most uses of lexical macros could be done with hygienic macros / macro replacement bodies that are fully formed expressions (AST nodes), but not all of them.

For those macros, really the "hygienic" doesn't matter half as much as people pretend. Don't do complex macros where the "hygienic" is required. Don't generate code, write code. If you need to generate syntax to remove boilerplate, then sprinkle a few macros in. But to generate syntax, in general you'll need a lexical macro system.

I'd say its not really a workaround, its a feature. And a hard one to use well. But as someone said in another comment, i think reading the preprocessor in C and especially C++ with the templating is really interesting and help a lot understand how the OG team behind the code used to work. Some tricks are "too clever" for me originally, but with proper documentation on these tricks, i understand tehm and sometime, i have a sudden hindsight/sudden clarity (i don't know how to say it, its a bit like entering the zone?)
Sounds like that it's more a workaround for lacking a proper macro system.
How is a straightforward solution to some development problem (create multiple language entities, like enum + data tables, from a single point of definition in the source code) a workaround for some lacking thing?

Maybe you could clarify by posting a better version of what GGP suggested.

code should always be written for other humans. it runs on the machine, but other humans will have to understand it.

dogma, in all circumstances, leads to intellectual laziness at best and malpractice as worst.

trying to express complicated ideas using simple constructs can result in complicated, verbose and very difficult to understand use of simple constructs. this is why we have complicated constructs, to simplify the expression of complicated ideas.

I agree with the larger point being made, but equally I’m getting a bit tired of statements like these:

> “ If it takes an hour to figure out what's going on, well, that's an hour that wasn't spent doing something else more useful and interesting.”

What exactly is “doing something else more useful”?

As a software engineer your main job (yes I know talking to stakeholders and blah) is to write and to read code. If as a software developer you optimise for writing as little code as possible and not having to spend much time reading code either then what exactly are you working on? It’s great if you’re a solo entrepreneur who wants to optimise their own time, but for most devs reading code for an hour is some of the best use of their time in their daily job.

(comment deleted)
> If as a software developer you optimise for writing as little code as possible and not having to spend much time reading code either then what exactly are you working on?

Minimizing code read/written _per unit of work_. The less code you have to work with for a particular task, the more tasks you can accomplish in a fixed time frame. I'd much rather read and understand three tasks in an hour than only one.

It still doesn't add up to me. I take your "less code" as more terse and straight to the point code, which would go towards "clever" code instead of long, verbose and "simple" code. Which would go against most of what she rants about.
We might further expand the model in my previous comment with "units of comprehension". You might do five operations (each costing one unit of comprehension) in one line or in five lines. If it's one terse clever line, that's basically the same thing as five simple lines because there's the same amount of comprehension cost. The clever line might actually be penalized if it's not immediately clear that it's doing five things at once (and if it's clever, that implies it's not immediately obvious).

However, you might instead pack those five simple lines into a function that you give a good descriptive name. Now although the computational units are unchanged, the work now sometimes only costs one comprehension unit if the programmer working with the code understands at a sufficient-for-their-needs level what's going on from the function name/signature without needing to delve into implementation details. The difference between that and the terse clever line is that the terse clever line is still implementation whereas the function is descriptive abstraction.

This is of course very rough pretend-math and there are plenty of real-world cases where function names aren't descriptive, but hopefully conveys the idea of how you can reduce the code you need to read without needing to resort to cleverness.

I think I get your point.

On a documenting perspective, having the well named function contain five lines of simple operations will look better than the terse 1-in-5 line.

To me the catch is that the reader/reviewer will need to trust the function actually does what it is named after. Otherwise they'll need to go look at the 5 lines anyway, and it might be more costly to go navigate to the function and come back than reading the same content inline.

With that 5-in-1 terser (that's such an horrible compression ratio BTW) line, there is no need for blind trust. It might need more effort to understand, but you also don't have the burden to associate the name of the function with what it actually does (even aptly named functions will still have some gap with what they do or don't). I'd hold my head while decrypting that 5-for-1 bundle while cursing the world, but I'd also see that as a very pragmatic and reasonable choice if it relies only on standard libraries and don't abuse undefined or deprecated behaviors.

BTW I'd still go with a separate function containing the longer code if it helps for tests for instance. As everything and as you point out, real-world cases are always more complex and nuanced.

Some of these articles miss the point: very often an expensive complication comes in order to solve a problem that otherwise would have been much lengthier or expensive to solve.

Just as an example: everyone would agree that react is more. Implied than jquery as it has more levels of indirections and abstractions, but it’s strong point has never been to show the cleverness of its authors but to solve state management issues and implement a serie of features otherwise not reachable from previous paradigms. Also for loops where more complex than gotos as they require state (invariant), but today for loops and other forms of loops are widely understood and teach during formal education.

New abstractions and indirections aren’t always necessary and when put in place they need additional documentation and training, but nothing of that was mention in the article.

One person's too-clever code is another person's attempt to prevent O(N^2) lines of unmaintainable noob code from being added.

Problem is, I've seen people with years of experience put this crap in. When you show them how much better it can be, they don't tend to take it well. It implies they're not the "senior engineers" their resumes claim.

They really aren't if they can't take criticism and feedback on board, or can't see how their code is objectively bad.

Senior is just a job title / pay grade, it does not mean someone is a good developer. Thankfully I've never had a label like junior/medior/senior attached to me, because while I have plenty of experience I don't think the job descriptions for senior developers applies to me. Probably impostor syndrome but still.

This feels all over the place.

Is she ranting against "clever" code ?

Or is it about "wicked, nasty, complicated, Klein-bottle-wannabe tricks", which wouldn't fit a "clever" category IMO ?

But she mentions "how far they read into the spec", so these are language features she doesn't know about ?

Oh, but "Nobody will ever have more state about the code than the original author" so it's not about syntax or tricks, but code states and complexity ?

What is there to take from this rant aside from her not liking her coworkers' code each in every possible ways ? I am really not sure to get it.

The article was a meditation on the Keep It Simple, Stupid (KISS) principle.

My grief is that, given time constraints, I'll put in a list comprehension rather than an explicit loop, where appropriate.

Am elevating the task above n00b level? Sure. Welcome to the real world.

> Nobody will ever have more state about the code than the original author at the time of creation.

I would add "including myself". It always happens when I go back to old code, even if it's my own code.

It's not that I don't understand my own code, or the lack of comments. But while coding (at least it happens to me) I have a global awareness state of what is going on, what's missing, what's failing, where I want to go, etc. You are plugged.

Losing and recovering that state is pretty hard and that's why (long) interrupts are so annoying.

A simpler language like C helps me recovering that state quickly. A mess with templates or 1000 levels of abstraction takes way longer.