Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Clever code is probably the worst code you could write (2023) (engineerscodex.com)
168 points by rbanffy on May 21, 2024 | hide | past | favorite | 198 comments


I think “clever” is more related to unfamiliarity.

There is actually a lot of cleverness going on that people just become familiar with.

Structured programming is actually very clever if you think about it.

Function calls are, when you look at it closely, very clever. It encapsulates how to jump to a function entry point, how to pass on values in registers or in memory, how to adjust stack pointers, and all other sorts of cleverness.

For loops are clever with different parts of the statement controlling and executing different parts of the loop.

Compare that with BASIC

A six year old child can understand:

    10 PRINT “Hello”
    20 GOTO 10
Going on to object oriented programming, dynamic dispatch and v-tables are clever. What you call and where you go to in your program are determined by the dynamic type of an object. This is very far from the simple BASIC GOTO

What difference does looking at it like this make.

First we don’t reject automatically reject new concepts just because they aren’t simple. While function calls are complex, they bring many benefits. In addition, this approach emphasizes the role of education to in taking useful concepts and making them familiar to a broader group of people.


I think there's a difference between "clever" and complex.

You can express a complex algorithm or pattern with simple easy to understand code - complexity doesn't have to manifest itself as unreadable or incomprehensible code.

To me "clever" code is more about they way you are doing something than the complexity of what you are trying to do. Clever is the opposite of straightforward and easy to comprehend without a detailed explanation.

For example you might be "forced" to write clever code as an optimization to calculate something in a non-obvious way, maybe also based on some non-obvious pre-conditions that have been assumed and make this a valid approach.

You don't normally want to write "clever" code - you want to write easy to understand straightforward code, and on the occasion when you feel compelled to a clever implementation, for the sake of future you or your teammates, you better precede it with a block comment prefixed with "here be dragons" and a detailed explanation of what it is doing and why it is doing it in this non-obvious way.


I also prefer this perspective. I had an epiphany about this sort of code when I was trying to describe what my code did for a research paper, and in trying to express why I was proud of it, I called it complex, only for my research advisor to point out that complexity was not the point of the work, so calling it complex did not convey what about it made the research interesting.

Although it wasn't his intention, it changed my perspective on the "clever" tricks I liked using, since it made me realize that being clever was not the kind of complexity that mattered. So, nowadays I try to write simple, easy to understand code, leaving the cleverness and complexity to the way the problem is tackled.


Just to codify this with some examples: Here’s some recent examples of what I consider “clever” that I’ve had to work with from previous people and that I’ve written myself:

Someone who loved Lisp wrote a bunch of the unit test suites where I work using Python in a very clever metaprogramming way. They would dynamically generate and attach functions to a test object for testing REST requests. This is both

1. Difficult to read and understand

2. Much more difficult to test the behavior of

All to save probably maybe 100 lines of code. This is an example where I feel like code is too clever for its own good without having a good reason to be like that. It also flies in the face of what you would conventionally expect when it comes to Python unit test suites.

One example where sometimes it’s necessary to be clever: I did a db migration in about 100 lines of Python/SQL that worked fine at small scale using Alembic/Python/SQL and was a straightforward update with CTE. When tested on large production grade dataset however it completely fell apart. Some clever hacks with batching and temp table later and I have something runnable, but now it’s all in sql and while well commented is much harder to grok what’s going on at first glance.


We've built modern computing on top of encapsulated cleverness.

I don't mind clever code, as long as it's a polished gem set into a nice abstraction hiding away the details.

Databases have clever code in them. Network stacks have clever code. Heap allocators have clever code.

We've built our simple code on top of these.

If you need some clever code, write it in the same style: a self-contained library with clean abstractions and thorough documentation.

Whatever you do, never "weave" clever code through simple business logic!


> Whatever you do, never "weave" clever code through simple business logic!

Oh I love that blog series.


I've always considered code to be "clever" when its using something in a way it is rarely used, and usually specifically with something very common like an arithmetic operator.

I've seen some really clever code using arithmetic operators to flip variables in ways that look like magic at first. I've also seen, and used myself, a few similar kinds of tricks in JavaScript especially when working with booleans.

I never really considered code "clever" when its just unreadable or incomprehensible. IMO that's just bad code.


> You don't normally want to write "clever" code - you want to write easy to understand straightforward code

I think what the parent comment is getting at is the relative nature of "clever" and "straightforward." At certain times and in certain programming communities, use of deep inheritance hierarchies was "straightforward," and passing a function as a method parameter was so "exotic" and "clever" that languages didn't directly support it.


I work with embedded DSPs, and there are certainly points where the maths gets incredibly dense and hard to intuitively parse. Your last point has proven true for me many times. Luckily, with the exception one block of code, the maths is detailed plainly above the implementation. So, while there might be an occasion where I might not fully understand what's going on, I can see where the original author of the code was coming from, and can follow the following code accordingly. Indeed, I believe one of the comment blocks does start with "Here be dragons".


Clever is simple, complexity is The Enemy.


Yes. A relevant quote from Alan Kay in his talk “the power of simplicity”:

> one of the things that's worked the best the last three, or four hundred years, is you get simplicity by finding a slightly more sophisticated building block to build your theories out of. its when you go for a simple building block that anybody understand through common sense, that is when you start screwing yourself right and left, because it just might not be able to ramify through the degrees of freedom and scaling you have to go through, and it's this inability to fix the building blocks that is one of the largest problems that computing has today in large organizations. people just won't do it

https://youtu.be/NdSD07U5uBs


Yes, but people are widely familiar with certain concepts. Hating "clever code" doesn't mean hating any abstraction or good elegant ideas (like function calls).

The thing is that this stuff has already been in circulation for so long, and many of the good ideas have been found. If you tried to come up with new solutions for function calls, people would understandably be skeptical.


Cleverness is like connecting dots.

It might be an instance that reveal a smooth curve that seems so obviously clear afterward but was unfathomed so far.

Or it can cast a baffling intricated sequence of discrete points each generated at coordinates using the previous one in a well specified but completely ungrabbable way, the whole drawing a scary screaming face that any sane mind will flea away from.


It's not this and there is a test:

Judging something assumes at least a minimal level of expertise in both the domain and language. I don't know Ruby, a lot looks obtuse, that's not a sign of clever code. If I see examples like the article in languages I know, it's 'clever code'.


But even then, there's degrees of knowledge and familiarity within that. For example, in Python, is using the `else` block of a for-loop clever? It's a part of the language that I'm familiar with - in principle it's just domain knowledge. But I've worked with plenty of Python devs who've never seen or used that construct before. For them, it's often black magic, and a classic example of "clever" code.

The point is that everyone judges cleverness based on what they know and are familiar with. If I need to think too hard to understand it, then it's clever code. But everyone has different levels of familiarity and experience, which means that cleverness is always an individual metric.


Yep. A long time ago I remember using C# delegates to pass methods into a standard error handling wrapper. It's part of the language, but this was before functional programming was part of most OO Devs toolkit. Everyone was completely confused by it.


Here's an old joke about the progression from junior to mid-level to senior developer:

Junior dev: My code is simple, straightforward, and easy to understand.

Mid-level dev: My code is clever, innovative, expressive, hyper-optimized, and ingenious.

Senior dev: My code is simple, straightforward, and easy to understand.

In software development, "clever" solutions are like poems. In the best poems, there are usually multiple layers of meaning, nuances and subtleties, some harder to tease out than others. Sometimes you have to sit with a poem for a while before you are able to truly drink it all in. To mid-level engineers, writing this sort of poetic code has an intoxicating appeal. It allows them to flaunt their talents, demonstrate their mastery of the language, and impress their colleagues with their ingenuity.

But more often than not, what is really needed is the code version of ordinary prose: straightforward, with a preference for clarity over succinctness, easy for others to understand, easy to edit, and with fewer surprises and deviations from convention than a poem. With prose, particular the sort of no-nonsense style found in wire news reports and explanatory journalism, the best work is easy for the reader to comprehend and lends itself to being edited. For instance, a skilled copy editor can condense it to fit, if need be.


I don't think it's accurate, though. Junior devs often write overly complicated code because they don't really understand the problem they're trying to solve. Junior devs write unintentionally complex code, mid-level devs write intentionally complex code, senior devs write simple code.


Sometimes seniour devs write complex code but hide the complexity behind a simple interface.


I'd include this as table stakes for a senior dev. You don't always need to work on complex code, but I'd expect they work to go to a senior dev and I'd expect complexity to be hidden behind a simple interface.


Hiding complexity behind a simple interface is not clever. It's smart.

It's also the facade design pattern.

https://en.wikipedia.org/wiki/Facade_pattern


With a slight difference that junior dev tends be proud of the code they’re added while a senior will be proud of the code they’ve removed…


So much this.


I just say, bell curve meme



I've seen junior devs write code carefully, putting time and effort into it. The code was ornate and almost too over-commented.

mid-level devs become more pragmatic, but can skimp on both elegance and simplicity vs complication.

What's interesting is that decent senior dev code sometimes almost looks careless, but really works well in the end. For example, immediately exiting with an error instead of complex error recovery. (the latter would just move the problem around and make finding and fixing the root cause more troublesome)

Another thing would be duplicating code instead of doing some complex re-use with complicated conditionals.


    What's interesting is that decent senior dev code 
    sometimes almost looks careless, but really works 
    well in the end.
I absolutely love this description.

In a lot of ways, YAGNI is what really sets senior code apart. When I was a more junior dev, I thought senior code often looked under-engineered, but in hindsight I realized I was over-engineering things at the time.

    Another thing would be duplicating code instead of doing 
    some complex re-use with complicated conditionals.
God, I wish I could go back in time and burn this into my brain.


Another example is some devs chain maps and array filters when a mundane for loop would do.


Mostly, yeah - although

Good poems give you clear meaning now, and deep meaning later.

I think probably great code is like that, too. It’s clear in how it addresses the immediate needs, but it’s deep in how it sets up addressing later, subtler, or less proximal needs.

Git itself comes to mind. At the basic level, it’s super straightforward: you have diffs, you put them in a row, that’s a branch. Easy. Clear.

Then you want to do something weird, and lo and behold, you can. The “cleverness” - maybe call it the “clever simplicity” - that it’s built out of makes it possible, and “easy”. The deep part of the poem, that you didn’t need to understand in the beginning, starts becoming apparent and meaningful.


Reality:

This is mostly because mid-level dev needs to justify their existence in order to not get laid off or PIP and is worried about losing their H1B and having to uproot their entire family in 60 days notice. Hyper-optimized, hard-to-read code that only they understand is one way to increase reliance on them while giving a reason that can be put into a promotion doc. Mid-level jobs are worried about maintaining their job.

Junior dev doesn't care because they can go wherever, they aren't worried about the uprooting, and well-written code is a ticket to a multiple new jobs.

Senior dev doesn't care because they have saved enough money, have permanent status, and if the company doesn't want them they aren't worried about there being better opportunities. They have enough online evidence of their competence and don't need to prove themselves.


When I was a mid-level dev, the overly complicated code I wrote came about because I read an article on e.g. Ruby metaprogramming, got excited, wondered why we didn't use reflection more, and found a place where I could apply it. Perhaps you thought about job security, but that seems like part of your personal journey and isn't tied to seniority. I was just inexperienced and a touch arrogant, as mid-level engineers must be.

At higher levels, the reason I don't write code like that is because I've been burnt too many times by the new hotness. It is slightly about job security, but only because I fully expect that any crazy shit I fling out today will eventually hit the fan and come back to me, probably at 3am during on-call.


This is mostly because mid-level dev needs to justify their existence in order to not get laid off or PIP and is worried about losing their H1B and having to uproot their entire family in 60 days notice. Hyper-optimized, hard-to-read code that only they understand is one way to increase reliance on them while giving a reason that can be put into a promotion doc. Mid-level jobs are worried about maintaining their job.

That seems to be a matter of survival overriding ethics and professional pride. I know I came up with some really cringeworthy and complex solutions as a mid-level dev, but I would never have done it deliberately.

If people are put into a situation where they need to purposefully write substandard code to not get deported, it's something that needs to be fixed.


I would argue that what constitutes clever code varies a lot by language. There's always a "cleverness" threshold where being able to read or refactor the code becomes harder, but this threshold isn't universal.

Python in particular makes it very easy to be too clever, since its extremely rigid syntax was designed specifically to discourage it, but it ended up giving the user the necessary tools to be clever anyway, and the end result is usually... not pretty.


What would qualify as clever Python? These kind of broad and vague statements make me wonder if I am guilty..


For a simple example, I think the walrus operator (:=) could be considered clever. I like it, and use it, but the fact that you can declare a variable, store a value in it, and then perform actions depending on its value, all in one line, gives me pause.

    if (foo := bar()) is not None:
        baz(foo)
Whereas the traditionally accepted Python method of dealing with this would be EAFP:

    try:
        foo = bar()
        baz(foo)
    except AttributeError:
        # handle exception


What? Those aren't equivalent at all.

Where are you expecting an AttributeError to come from? Why are you comfortable catching them from anything inside of the baz(...) invocation?

The traditional method would be to just bind it and check for a null outside the expression.

    foo = bar()
    if foo is not None:
        baz(foo)
I don't know why people insisted on pretending binding variables before using them was such a difficulty that it was worth altering the language. Lazy and bad programmers aren't going to stop being lazy and bad when you hand them the ability to name things willy nilly. They'll just use that badly as well.


The assumption in the example is that baz() required foo to not be None; attempting to use None where you expect a string will probably in an AttributeError.

As to your example, both LBYL and EAFP are accepted Python standards.


In your first example I think most people would skip the 'is not None' as None is already falsy and drop the extra braces.

I find it visually clearer than the second example where the cause and effect are slightly separated, but I do agree there's an element of cleverness to it.


But that would be a mistake if you need the branch to run for other falsely values like 0 or the empty string.


In this imaginary example, yes, a False is likely to cause the same issues that None would. But as a sibling comment mentions, it’s not always desirable to drop the explicitness for syntactic sugar.


I currently have a dilemma regarding a XML generation code. Should I write it in pure Python with endless nested loops and DOM manipulations. Or should I use a templating engine that is exactly the DSL [:domain specific language] to express declaratively how to generate the output.

Solution 2 is superbly consise and straight to the point, but I am pretty sure noone will ever climb the learning curve that this additional DSL imposes to any newcomer.

I really wonder which final choice I will make for my production code.

[truth is that refactoring solution 2 is painless, but at the same time debugging solution 2 is tricky]


Can you write functions and classes in Python that roughly mimic the DSL you're aiming for? This is often called an embedded DSL, and it's a great technique for allowing developers to work with domain objects inside a language that they're still familiar with.

There's a handful of templating languages that do this sort of thing using functions, so in Python you might write

    form(
        {"method": "POST"},
        label(
            "Name",
            input({"type": "text"}),
        ),
    )
The learning curve becomes significantly lighter because you're just using Python constructs in the Python language, which means your IDE can suggest functions to you like it would with any other library.


TL;DR

- OOP obsession is a common python developer phase. it's a dangerous phase.

- maintainable code is not minified code. minified code is minified code.

- stoopid code is often stoopid enough when it satisfies real world / human concerns, not technical concerns.

----

I've done all of these, and I see other people repeating them.

1. hyper optimised and utterly fragile class based inheritance / abstractions. Not optimised in performance. Optimised in terms of minimisation of code. avoid ABCs like the plague. YAGNI so save yourself some heartache and keep it stoopid.

2. especially when ^ includes many static methods that could be standalone functions. a good sign the code can probably refactor to functional + objects/dataclasses (and probably be easier to test as a result). You didn't need it, go back to keeping it stoopid.

3. methods that call another method, that calls another method, that eventually calls one of the static methods. often when I see this, none of these child methods are called by anything else. someone wrote multiple separate methods because apparently we shouldn't write methods with more than 10 LoC. because that's how to write clean code apparently. just put it all in a single method so I don't have open multiple different browser tabs while sitting in the doctor's office responding to an incident on my phone. stop optimising for LoC and make it obviously stoopid.

4. hiding how the code will run away from main entrypoint by adding a `className.run()` method. yeah, cool, your main function has been minified. kudos. But now I have no idea what steps your script will run. I have to go and read something else. make it obviously stoopid to someone reading this for the first time.

5. using names of concepts from other languages. don't call classes an "Interface" or a "Controller" because it sounds better. This isn't Java nor is it Kubernetes. It's python. keep names so stoopid that I can understand when my phone has woken me up at 3 am.

6. functional is usually simpler, until you start turning in a mathematician. you are not a mathematician. and neither is the junior sitting next to you. don't overuse recursion or currying etc. keep it stoopid enough that the junior sitting next to you has a chance of taking over responsibility for it one day without going through a PhD in mathematics.

7. avoid using functionality from the last 3x minor versions of python [0]. slow down and let others catch up first so we can all be stoopid together.

----

caveat: experience will vary wildly between different hoomans regarding what is considered stoopid enough.

[0]: a good exception here is something like case matching. this was pretty big so I would have allowed that, so long as everyone was aware it was a new thing now (I'd have done a post on slack saying -- Oi, go look at this, it's big)


I find functional programming is where things get bad. It does give some pretty elegant ways to do things, but it can become a nightmare when debugging. Sometimes I need to dig into Django code to work out what's going on. It's all fine while the code is imperative, but as soon as you hit a functional part, then following what's going on gets difficult.


> It's all fine while the code is imperative

ah sorry, i may have had a brain fart here. I use functions imperatively like you say and that's the concept i wanted to get across. failed to do so because I just mentioned don't over-do it on the math.

my bad.

should probably have been an extra one between 6 + 7 for do things imperatively


Coming from Perl, Python made me write more verbose simple code. But I noticed that in Perl I would do something clever, then a few days later it would come back and bit me in the arse, when I was trying to debug it. Sure Perl saved a few lines of code over Python, but my Python code ended up more reliable. I do miss Perl though.


I also find that, in C++,

    int sum = 0;
    for (int i = 0; i < n; ++i)
        sum += x[i];
    return sum
is a lot easier to understand than

    return std::accumulate(x.begin(), x.end(), 0, [](int a, b) {return a + b;});
Yet, the latter is considered more correct and better, with static analysis like cppcheck telling you to use the latter. It does have many advantages, like no mutable variables lying around, but gee it is annoying to read.


We now have

       return std::reduce(x.begin(), x.end());
Which is a little cleaner and is even faster (compiler is free to do the additions in any order). https://en.cppreference.com/w/cpp/algorithm/reduce - it looks like even the `accumulate` example can be made simpler with `std::plus`. I prefer the `reduce` option for a number of reasons, but understand why someone might not.


That's terrible! The main operation, addition, is completely hidden magic! But the completely trivial calls to .begin() and .end() are explicit!

Why would I want to add by default?


> The main operation, addition, is completely hidden magic

the main operation is reduction which is fairly basic computer science and taught in any non-BS curriculum - https://en.wikipedia.org/wiki/Reduction_operator (or https://en.wikipedia.org/wiki/Fold_(higher-order_function) which is the standard way to introduce this concept ; check in particular the table showcasing all the implementations in various languages). The standard reduction for a set of numbers to a number that is likely going to be the very first example in your textbook is addition.


The reduction is explicit. That's not what the complaint is about. And it's more about implementation than the actual result.

The actual operation is addition. I don't care if it's the first example, that doesn't mean you should hide it.

Imagine if print did hello world by default.


Maybe the idea is that you overload your favored default as a + operator? Even more baffling certainly, but at this point I wouldn’t even be surprised this was considered a compelling argument in the mind of whoever settled this API choice. :D


std::ranges::fold_left (since C++23) can take a range directly instead of a separated pair of iterators, and requires the explicit add.


What is hidden? It's a reduce on + starting with 0, hardly anything subtle.


>the `accumulate` example can be made simpler with `std::plus`

Accumulate, surprisingly enough, accumulates by default:

    return accumulate(x.begin(), x.end(), 0);


Just make sure you're accumulating integers and not doubles!


  #include<cstdio>
  #include<numeric>
  #include<vector>
  
  using namespace std;
  
  template <class T, template<class> class Cont>
  T sum(Cont<T> x)
  {
    return accumulate(x.begin(), x.end(), (T)0);
  }
    
  int main(int argc, char *argv[])
  {
    vector<double> ds({1.1, 2.2, 3.3, 4.4}); 
    vector<float> fs({1.1f, 2.2f, 3.3f, 4.4f});
    vector<int> is({1, 2, 3, 4});
    vector<unsigned long long> ulls({1ul, 2ul, 3ul, 4ul});

    printf("doubles: %f\n",sum(ds));
    printf("floats: %f\n",sum(fs));
    printf("ints: %d\n",sum(is));
    printf("ulls: %llu\n",sum(ulls));

    return 0;
  }


Obviously, you have to use

   std::accumulate(v.cbegin(), v.cend(), decltype(*v.cbegin()){});
:) :) :)


> and is even faster (compiler is free to do the additions in any order)

Is that actually true? I'm not even sure how hypothetically removing ordering requirements would help you extract performance, let alone any compilers that could do anything with that today. Unless the standard library were to auto-parallelize the reduction, but I doubt they'd do that because the overhead of starting threads would be quite costly for anything but the absolute largest ranges since C++ doesn't have a thread pool sitting idly for you (not to mention that the docs for the function don't mention any thread safety requirements for the BinaryOp and Init which would be required for any such optimization).


I think relaxing the ordering requirement let's you use simd something like this (semi-pseudo code)

  (a, b, c, d) = (0, 0, 0, 0);
  for(int i = 0; i < n; i += 4) {
    (a, b, c, d) += (arr[i], arr[i+1], arr[i+2], arr[i+3]);
  }
  return a + b + c + d;


What if n is not a multiple of 4? There is a trick I can't recall at the moment for handling this with a case statement on n mod 4 at the end to wrap this up.



Are you sure that the compiler doesn’t autovectorize a simple loop into this form anyway? This is kind of the defacto scenario for that pass.


This transformation is only legal if order of summation doesn't matter. So the compiler can do it for ints, but not floats.


Applying std::reduce to floats would be a similarly bad idea for having the compiler do it, except people will write it without thinking. Of course, people always struggle with writing correct floating point so I doubt it makes things better or worse, but it’s conceptually the library equivalent of -fast-math (which coincidentally would also auto-vectorize the loop but can impact more than just that).


Using fast math in one particular expression is way different and less risky than doing it globally.


You answered the question yourself..can you at least edit your comment so you won’t confuse otherwise correctly oriented computer science practitioners?

The commutative property is fundamental to understanding algebraic structures and fast parallel processing. If a binary operation is commutative over a given set then, yes, a proper compiler will parallelize it. No, it will not necessarily use OS threads directly one to one, that’s usually not a consideration even for a naive approach.


The linked page has benchmarks - I'd have to go dig into godbolt to see why it's faster. The standard library can actually auto-parallelize, but you have to opt in (see the ExecutionPolicy argument).

Edit: I don't know how to dig into the actual implementation of std::reduce on godbolt - it's not inlined. I think the sibling comment has it right though - one can do adds of four at a time with whatever SIMD extensions are available.


I think most people would be thinking "hmm now I need to read what 'reduce' mean in this context"


At least in my experience in the C++ world there is a general expectation that you learn the "standard algorithms" and prefer them over the alternatives unless you have a specific reason not to.

For example: outside the C++ ecosystem, most people probably would stare blank faced if they saw `std::rotate` in a codebase but it's basically a meme at this point in the C++ space.

Ex: https://www.youtube.com/watch?v=UZmeDQL4LaE


For at least the first time, yeah.


Look at that link:

  2,4,6) ...
  These overloads participate in overload resolution only if
  std::is_execution_policy_v<std::decay_t<ExecutionPolicy>> is true.
  std::is_execution_policy_v<std::remove_cvref_t<ExecutionPolicy>> is true.
std::is_execution_policy_v? std::decay_t? std::remove_cvref_t?

Really, this madness with C++ needs to stop. People who work on newer versions of the language might be very conservative with the language syntax itself, but they clearly decided having carte blanche to adding an infinite amount of stuff to the standard library.

Could someone please force them to stop, somehow? The whole rest of the industry would be thankful.


It's mostly the problem of the language, and not of the approach.

In a more expressive language you can omit the explicit slicing (x.begin(), x.end()) and have the compiler derive the lambda's signature for you, so you'd write something like reduce(x, (a, b) => a + b), or even fold (+) x, with all the same static analysis and efficient compilation guarantees.


I think it's mostly a fad issue. Normal loops and if statements are just as possible to hit with a static analyzer. But the very fact that they look easy makes a certain kind of programmer see them as beneath them. They want the complex looking code, even if it's functionally equivalent and semantically no more sound. They like the visual noise and complexity of it. It rubs their egos the right way.


Loops are more complex. They expose more implementation details, worse, they "expose" irrelevant details. Unless your CPU is very simple, like a Cortex M0, the C compiler will likely rewrite your loop using vector instructions (think MMX / SSE / Neon), leaving an unrecognizable mess where a neat loop with an index used to be.

C was invented to match PDP-9 and PDP-11, and it matches them beautifully. Constructs like *a++ = *b++ directly compile to instructions like MOV (R1)+, (R2)+, etc. But however much I may like the beauty and simplicity of the PDP-11 architecture, it belongs to the past, or maybe to simplest MCUs. C no longer matches hardware all that well, and especially the idioms of C from classic books written 30-40 years ago don't match modern hardware woefully, if you care about the last bit of performance. (If you don't, take C#, Java, Go, even V8; they are plenty fast with JIT compilers.)


    00106   template<typename _InputIterator, typename _Tp, typename _BinaryOperation>
    00107     _Tp
    00108     accumulate(_InputIterator __first, _InputIterator __last, _Tp __init,
    00109            _BinaryOperation __binary_op)
    00110     {
    00111       // concept requirements
    00112       __glibcxx_function_requires(_InputIteratorConcept<_InputIterator>)
    00113       __glibcxx_requires_valid_range(__first, __last);
    00114 
    00115       for (; __first != __last; ++__first)
    00116     __init = __binary_op(__init, *__first);
    00117       return __init;
    00118     }
    
    https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.0/stl__numeric_8h-source.html#l00108
It's just a loop.

The compiler is doing all of the same transformations regardless of whether you use the higher order function or just write a loop yourself.

--------------

    _STD_BEGIN
    _EXPORT_STD template <class _InIt, class _Ty, class _Fn>
    _NODISCARD _CONSTEXPR20 _Ty accumulate(const _InIt _First, const _InIt _Last, _Ty _Val, _Fn _Reduce_op) {
        // return noncommutative and nonassociative reduction of _Val and all in [_First, _Last), using _Reduce_op
        _STD _Adl_verify_range(_First, _Last);
        auto _UFirst      = _STD _Get_unwrapped(_First);
        const auto _ULast = _STD _Get_unwrapped(_Last);
        for (; _UFirst != _ULast; ++_UFirst) {
    #if _HAS_CXX20
            _Val = _Reduce_op(_STD move(_Val), *_UFirst);
    #else // ^^^ _HAS_CXX20 / !_HAS_CXX20 vvv
            _Val = _Reduce_op(_Val, *_UFirst);
    #endif // ^^^ !_HAS_CXX20 ^^^
        }
        return _Val;
    }
    
    https://github.com/microsoft/STL/blob/63354c3fa9c1fb2ab1fccb58c47d23c6af1c290f/stl/inc/numeric#L24
Also just a loop in MS' standard lib.


Try -O2 or -O3 like you would in a release build.

See https://stackoverflow.com/a/50786881


Optimization levels aren't going to care if you're using std::accumulate or not.

They'll either analyze the loop directly, or they'll analyze it after munging it through the template function instantiation code and inlining the results of the template function.

After that, both are doing what you originally said, lifting the C into an abstract form and analyzing it for whether parallel computation can be applied.

Hell, the code to SSE enabled asm examples they give in the comment you link are just plain for loops.


Nice dissertation but loops is all you need.


From a certain standpoint, a Turing machine is all you need.

(Heh, even Malbolge Unshackled is likely Turing-complete.)


You got me


That is somewhat annoying, yes. I'm a huge fan of Python's list comprehension, but it's generally accepted to be a normal part of the language, and IMO, more readable:

    print([x for x in range(10) if not x % 2])
    [0, 2, 4, 6, 8]
vs.

    l = []    
    for x in range(10):
        if not x % 2:
            l.append(x)
    print(l)
    [0, 2, 4, 6, 8]


Call me crazy, but I find the second example more readable.

The first example is long line and you have to go back and forth to understand it. The second example is a single top down pass.


I’m sure I’m biased, since I code mainly in Python. List/dict comprehensions read the same to me as the longer form, just more concisely. This can be taken too far, and you can wind up with horribly obtuse one-liners that are just awful.


I write and maintain code in a bunch of languages. Rust has risen to the first place, C is the close second followed by C++ and Python.

All of the languages except C have similar iterator-based stuff that let you write one liners and often with lambdas. I dislike them all. They give way too much leeway and encourage many developers to try to prove how clever they are.

Once you have to debug the code containing them, all of the complexity of the syntactic sugar comes crashing down on you. The debugger starts jumping to weird places, sometimes even optimized out parts of the standard libraries while for loops usually stay debugable.


Agreed, me as well.

The second example reflects how my brain thinks and the intuitive order of what has to happen.

In the first example, it's all out of order.


I've seen people complain about wrapping a comprehension over multiple lines, but I can't imagine packing them all in one. In your example, I would spread it out over five lines, so it would be at as long as the imperative case.

While I like python, I think the unusual order that components of a comprehension are written is to its detriment. It would make more sense if they were in the same order in both examples. The python ternary has a similar issue.


You chose the one example that doesn't need a list comprehension. print(list(range(0, 10, 2)))


Good call.


With ruby 3.4 (prior to that `_1` should be used instead of `it`):

    (0..10).select{it.even?}


Also not that the result is `[0, 2, 4, 6, 8, 10]`, unlike the behavior of Python which exclude the value explicitely passed in parameter from the represented range.


    x = np.arange(0,10,1)
    print(x[x%2==0])


    int sum = 0;
    for (int i = 0; i < n; ++i)
        sum += x[i];
Pretty much looks the same in all C-like languages. I've written that in Java, Go, TypeScript, PHP, etc...

On the other hand, that second 'clever' example always looks different for every stupid language. It's std::accumulate in C++, streams in Java, list comprehension in python, etc...

Clear is better than clever.


One man's clever is another man's clear.

https://web.archive.org/web/20180619022832/http://webdocs.cs...

"The instructions corresponding to a conventional language might be expressed something like the following:

Select an apple from the box. If it is good, set it aside in some place reserved for the good apples; if it is not good, discard it. Select a second apple; if it is good put it in the reserved place, and if it is not good discard it. ... Continue in this manner examining each apple in turn until all of the good apples have been selected. In summary, then, examine the apples one at a time starting with the first one we pick up and finishing with the last one in the box until we have selected and set aside all of the good apples. On the other hand the instructions corresponding to an array language could be stated simply as “Select all of the good apples from the box.” Of course, the apples would still have to be examined individually, but the apple-by-apple details could be left to the helper.

Conventional programming languages may be considered then “one-apple-at-a-time” languages with machine languages being a very primitive form, whereas array languages may be considered “all-the-apples-at-once” languages. In two of the following three sections we shall consider the array languages APL and its “modern dialect” J with an intervening section giving a short discussion of the array language Nial which was influenced by APL."


> Pretty much looks the same in all C-like languages

Because it’s ancient; that doesn’t necessarily make it better or clearer. Look at it like you’ve never seen it before. It has an entire line of boilerplate smack bang in the middle. Sure, your brain filters it out because you’re used to it, but it’s still fugly. And it’s prone to typos/copypaste errors like all boilerplate.


Yes, but it's also not going anywhere.

I don't consider replacing a common loop convention with what amounts to a randomly-generated sentence or two of syntax and letters in each language to be an improvement.


Just the '++i' instead of 'i++' brought my reading that code to a halt and I had to start deciphering if it actually changes the operation of the loop.


I like C++, but I don't understand this tradition of keeping these annoying namespaces prefixed to everything. Just get rid of those std::, foo::bar::whatever::, etc from your code and make it more readable. Use the "use" clause. It's very rare for such names to be ambiguous in the same file, unless I'm missing some bigger picture here.


    using namespace std;
is considered kinda bad since you don't want your namespace to be polluted with a bunch of std stuff. For example if you have `int count` lying around somewhere you'd want to be able to call `std::count` without fear of it being shadowed.


But ambiguity gives you readability. And if a "count" variable clashes with the function name, it's trivial for the compiler to catch this and warn you.

It's a matter of preference, but I'd still risk ambiguity and name collision, especially when you go beyond std:: (like boost stuff).


Because of bad design decisions, a whole pile of unrelated stuff lives in the std namespace, and the disambiguation probably isn't what you wanted, so this can silently cause surprises.


It gives you readability. It's not what I'm used to, so it feels like reading French to me. Generally, if I see something without a namespace, it has a local scope.


Why would you not use std::sum(x) ?


That is just a question of what you are used to. The "functional style" example is very obvious to a person who is familiar with that style. In fact if the rest of the code base is in a similar style it is the easier one to understand.

There is nothing inherent which makes either better or cleverer than the other.

(Also the first one is incomplete, as "n" is not defined, making the later more self contained)


Traditional C style for is terrible compared to what you can do with iterators in my opinion, but idk if C++ has those.

    list.iter().sum()
ezpz


C++ makes this (and many other things!) needlessly painful.

In C# it is just

    return numbers.Sum();


Same for Rust.


I tend to use for loops in Rust for this reason; simple, and understandable by anyone who's programmed in an imperative language.

The one-liner approach tends to include an explicit type declaration (Or turbofish), `iter()`, and `collect()`, at minimum.


FYI I think you've misinterpreted the parent. To my eyes it looks like they're agreeing that Rust makes summing a list easy using combinations. For example, this is how I would do it in Rust:

  x.iter().sum()


I think they’ve understood and are claiming that a simple loop is cleaner than what you wrote. I’m not sure how since that 1 line communicates what’s happening at a high level with 1 word “sum” whereas a loop requires me to read the loop to confirm it’s a plain summation.

This sounds like someone complaining that the concepts they learned when they were first starting out are the only concepts that programming languages should introduce which is silly - technology evolves as we find better and new ways of accomplishing old tasks.


Where did "numbers" come from, and why are you so sure you can Sum() it? The original C code offered has some data structure (perhaps an array?) called x. Do C# arrays have a Sum method? I don't think so.

In Rust you would probably just write: x.iter().sum()


> Do C# arrays have a Sum method? I don't think so.

It's not literally a method (it's not, for instance, in the vtable of some array class), but as far as their API is concerned, yeah, they do: https://github.com/dotnet/runtime/blob/main/src/libraries/Sy...


C# arrays don't. However, C# arrays are IEnumerable<T>, which does.


IEnunerable<T> of course does not have Sum.

Only collections of numeric types (including arrays) have Sum.

https://learn.microsoft.com/en-us/dotnet/api/system.linq.enu...


So you can't use IEnumerable<T>.Sum if T is a custom type implementing operator+ ?


You could, but .Sum is a little more specific than that - it is non-overflowing sum.

Historically, constraining generic arguments on addition was problematic - the full feature set of numeric types was "lifted" to be fully representable through generics only recently[0].

With that said, there is an open proposal[1] to introduce additional generic math overloads to IEnumerable<T> methods, but it hasn't seen much activity as the existing overloads cover most commonly used numeric types already.

[0]: https://learn.microsoft.com/en-us/dotnet/standard/generics/m...

[1]: https://github.com/dotnet/runtime/issues/64031


This comment makes no sense. You dismissed the C# code, and then wrote the same thing in Rust, but with an extra non-conceptually-meaningful boilerplate step.

May as well ask, "where did x come from, and why are you so sure you can iter().sum() it?"

C# has generic types, so yes, C# arrays of numbers have a Sum method.

https://stackoverflow.com/questions/2419343/how-to-sum-up-an...

Don't make bold dismissive comments about things you are ignorant of. It makes you look Blubby.


The C# code ends up relying on LINQ, it's interesting how many C# programmers don't even think about that, either anything they work on already uses LINQ or they just reflexively bring it in everywhere they write C#

You'll see that a few of those SO comments actually say they're relying on LINQ to make that work. The array type doesn't have such a method itself.

So, in reality although many C# programmers will think of this as "correct" it just won't even compile... except if there's already LINQ.


There is nothing wrong with System.Linq just like there's nothing wrong with Rust's std::iter::Iterator. If anything, this makes writing Rust use the existing muscle memory if you have C# experience and vice versa.

The performance profile of LINQ, while much maligned, has been steadily improving over the years and, in the example of Sum itself, you actually do want to use it because it will sum faster than open-coded loop[0].

I do have grievances regarding LINQ still - my (non-negotiable) standpoint is that Roslyn (C# compiler) must lower non-escaping LINQ operations to open-coded loops, inline lambdas at IL level and similar, making it zero-cost - .NET (IL compiler/runtime) provides all the tools necessary to match what Rust's zero-cost-ish iterator expressions offer and there just needs to be more political will in the Roslyn teams to do so. Because of this, I'm holding my breath waiting for DistIL[1] to be production-ready which does just that.

[0]: https://github.com/dotnet/runtime/blob/main/src/libraries/Sy...

[1]: https://github.com/dubiousconst282/DistIL

(I don't understand what you mean by "it won't compile", because it will, in most cases System.Linq namespace is already referenced anyway, either through global usings or at the top of the file)


> (I don't understand what you mean by "it won't compile", because it will, in most cases System.Linq namespace is already referenced anyway, either through global usings or at the top of the file)

The "in most cases" is doing all the lifting here. Rust's Iterator is in the prelude. Even if you #![no_std] you get the core prelude which has core::iter::Iterator. You would need to explicitly write code to tell Rust "No, I don't want the prelude" to get rid of it, I'm sure people do that, otherwise it wouldn't be possible, but few enough that I've never seen it.

But in C# there is no such promise. In most (but not all) real world C# projects somebody already brought in LINQ. If you're using a technology that gives you a "ready to go" standard C# project template it undoubtedly folds in LINQ too. But it's not actually provided by the language and that's a meaningful gap.

Notably in several of the playground type tools, since LINQ is not there by default this won't work - like I said it won't compile and it doesn't suggest "Oh you need LINQ" because the C# compiler doesn't provide such suggestions.


It's difficult to talk to someone committed to misreading the replies.

LINQ aka IEnumerable/Iterator methods are usually imported by default, something like with prelude. As others said, they are part of standard library.

I'm not sure what you are arguing against either but whatever your criticism is - it is misplaced as C# and Rust roughly belong to the same Venn diagram of features against commonly held beliefs.

You can test it yourself:

    sudo apt install dotnet-sdk-8.0
    mkdir TestConsole && cd TestConsole
    dotnet new console
And then just paste/echo the following to Program.cs:

    var numbers = Enumerable.Range(0, 10).ToArray();
    var sum = numbers.Sum();
    Console.WriteLine(sum);
This will compile. If that's not enough, then it's likely Rust the language is not for you either and you may want to use something like Go for the time being.


LINQ is just part of .NET standard library along with the very collections we are talking about. I'm not sure why one would care about the distinction.


Usage of LINQ is usually taken for granted.


sum(x) is easiest to read. If your language doesn't provide that as standard just define it yourself. In general you should always be idiomatic, though. If every C++ developer understands the accumulate version then use that.


the readability issue here is in part that function arguments are purely positional in c++.

a little over the top for this simple example, but you can always create local variables to document intention.

  auto initVal = 0;
  auto accumulateOp = [](int a, b) {return a + b;};
  return std::accumulate(x.begin(), x.end(), initVal, accumulateOp);
I'd prefer to see a for each loop over algorithms functions in such simple cases, but I'd prefer almost anything over direct indexing when it isn't necessary. when I see that in new code, my first thought is always "what am I missing here?".


One pet peeve of mine is people using std::for_each instead of a simple loop


Same in JS. I once worked with someone who would use [].forEach and didn't like for loops because functional good, iterative loops bad. It was essentially in the coding style of the project and the for version would be rejected in MRs.

I don't miss this.

IMHO [].forEach just essentially expresses the same thing as a regular for loop, although in a more verbose and convoluted way that is also likely very inefficient because a JS compiler probably can't optimize away the function call per iteration, because of the very dynamic nature of JS. forEach probably has its use when you specifically need to call an existing function on each element, but I don't otherwise see the appeal.


You are making a strong evidence-free claim about what JS compilers don't do.

https://stackoverflow.com/questions/9981607/array-foreach-ru...


You call "strong claim" something I prefixed with "probably"?

It seems for loops in your link can be slower because .length (a function in disguise) is repeatedly called. I do save .length when writing JS. .length calls has been hard to optimize and it can't hurt too much to cache it at the start of the loop.

Now it's good news if today's engines are able to optimize most of .forEach. The amount of optimization JS engines can do never ceases to amaze me and yes, any performance claim in JS needs to be constantly reviewed and checked at the time it is discussed.

You can't take a "probably" as gospel. Still, it was a bit lazy of me to not re-check, I guess being called out was warranted.


At $WORK we use Mozilla Rhino for some JavaScript processing.

We tested the normal loop vs. the clever loop. Performance-wise, the normal loop blew the doors off the clever loop.


Rhino is a very uncommon engine though. It has its niche, but if you're developing for simpler engines like that, then you typically know that in advance and know what considerations you're going to need to make for that.

When it comes to performance, lessons that apply to Rhino are unlikely to apply to more mainstream engines.

EDIT: That's not to say that everyone should use map and forEach all the time, just that your benchmarks are unlikely to be relevant to most JS devs outside of your specific use-case.


The other thing nice about the second style is that it's much easier to skim when part of a larger function, since it's a single type-checked statement. Keeps you from doing too much in it too.


If you use C++23's ranges and Boost Lambda and sacrifice your better judgement, you can get it down to:

    fold_left(x, 0, _1 + _2)


Imo it doesn’t matter at all. Both are fine. Surely there are more pressing things out there to think about!


I haven’t written any C++ since the 90s. That is unrecognizable. Is that an anonymous function?


C++98 should be forgotten as a bad dream. C++17 is almost a sane and convenient language (as long as you remember about the boiling depths into which you can fall if you're careless).

C++ will live for very long, like Fortran. And also like Fortran, there now must be a serious and uncommon reason to start a new project in it.


Why does one need an uncommon reason to begin development in Fortran?


For instance, the much narrower circle of experts. Usually if you do need Fortran, it means you're going to run numerical code on a huge cluster with RAM in terabytes and cores in the thousands, so the developers you're looking for should understand parallelization very well (and how modern Fortran does it), know words like MPI and Slurm, etc. Beside that, knowledge of the common numerical methods and libraries is expected. This is a relatively unusual setup. Few job listings mention it.

If you just want some highly parallel numerical code, but a GPU with several tens of gigs of RAM would suffice, you just take Numpy, or PyTorch, or other such library, wrapped into Python. You suddenly have a wide circle of developers, plethora of references, and no lower chances to publish in a prestigious journal than if you'd taken Fortran :)



Kernighan's Law:

Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.


I'm finding LLMs invaluable in deciphering this kind of thing, with suitable prompting:

System: Always provide clear instructions from the perspective of an expert in the field. Double-check answers for logical coherence and factual correctness, using a consistent step-by-step methodology. Explain the benefits and disadvantages of different approaches in a compare-and-contrast style, and value security, readability, and organization over clever tricks.

User: Consider the following code function in Python: "def minimumTotal(self, t): return reduce (lambda a,b:[f + min (d,e) for d,e,f in zip(a, a[[1:],b)], t[::-1])[0]" . The goal is to rewrite this using only simple python bulitins like for loops. Use a step-by-step approach to dissassemble this code into a simpler format, and include plenty of comments.

User: Clarify what is meant, mathematically, by "the minimum path sum from top to bottom in a triangle"

Okay now I understand it... This is much easier if the original code has a set of robust tests you can run your LLM-generated code against, to make sure it works as advertised, but you can get the LLM to generate tests too if needed.

Now I want to go see how it does when faced with C obfuscation competitions.


Unless you got dumber between writing and debugging, it more likely means that it takes twice as long (or even more if you haven't touched it in a bit). It's unlikely that Kernighan meant it takes someone twice as smart to figure out what you were doing as that would be a nonsensical interpretation (someone twice as smart may not be able to figure out what the stupid person is trying to do in the first place if the code written was nonsensical).


> It's unlikely that Kernighan meant it takes someone twice as smart to figure out what you were doing

no, that is exactly what he meant. clever code means you are just barely able to understand it enough to write it yourself [in any amount of time]. therefore, you aren't going to be able to debug it at all, by a factor of nearly two. and if you are the "smartest" person in the org (which he often was), then you are really in big trouble.

now obviously things in the real world like "smart" and "clever" are not one-dimensional quantities in neat categories, but he was making a memorable and funny quote, with quite a bit of truth to it, not a precise scientific hypothesis.

see also: "too smart for one's own good"

> nonsensical interpretation

how so?

> someone twice as smart may not be able to figure out what the stupid person is trying to do in the first place if the code written was nonsensical.

true, but irrelevant, this is about clever code from a smart person, not nonsense code from a stupid one.


The Linux kernel for example is filled with code that is at the limit of cleverness of the people who came up with it (e.g. RCU or intrusive linked lists). Turns out that once a concept is introduced, people can absorb it and become smarter. It’s just simply categorically not true that something written at the limit of your cleverness at one time prevents you from being able to debug it because cleverness is not something fixed and static.


agree, but as i said: "he was making a memorable and funny quote, with quite a bit of truth to it, not a precise scientific hypothesis"

never-the-less there is still a lot of truth to the saying, nobody said it was universally categorically true.

i've written clever code when it needs to be clever, for example performance. along the lines of the RCU code. when i do i plan for handling the complexity - static analysis or exhaustive testing if possible.

suddenly needing to advance your cleverness by 2x isn't impossible, but it's not fun if your butt is on the line.


Sure. I wasn’t advocating for making all your code clever. But clearly super clever code doesn’t make code magically not debuggable. It may not be fun to touch but usually you just make sure it’s well tested, documented, & then you try to never touch it again.


Most of the problems I've had to debug the place to look is not clear. Sure I've had to fix "The is not spelled Teh", but most problems are it breaks in some weird situation - step one is figuring out where to even start looking. Once I narrow down the exact place to look it might be easy, but I have to hold a lot of different areas of code in my head while narrowing things down. Even when I narrow it down, sometimes the answer is make the code more complex, and if the code is already on the edge of how complex I can handle I won't know how to make it more complex.


not smart enough to debug it in the same amount of time it took to write it


I keep to a simple rule: smart code is an asset, clever code is a liability.

Smart code is usually simple and clear, while also being short and efficient. Achieving this is not easy, but reading, understanding, and using smart code is easy. (Otherwise it's not smart, but, well, ordinary.) An example of smart code for me would be the merge sort algorithm, or the Lisp interpretation loop.

Clever code is usually some kind of last-resort hack, a trick, applied in dire straits, or to achieve a unique effect. An example would be the inverse square root hack, or the Duff device.

Beside the actual writing time, code review time is good for making code smarter and less clever.


"Clever code" here means "code-golf-like code".

"Clever" is too subjective to be used like this. One person's clever, is another person's mundane, and it varies across languages, ecosystems, teams.

Also I'd like to point out that in this example, had that function had a comment and a couple of tests, then it wouldn't really matter much what the implementation is. If you don't like it, you can rewrite it your preferred way.

Though as a dev I'd be too lazy to try to optimize code for code-golf-like properties in the first place.


This is true, but in code reviews and such, it often boils down to familiarity above anything else, like someone preferring

    names = []
    for record in records:
        names.append(record["name"])
to

    names = [record["name"] for record in records]
Now, I might say something if I saw this:

    import operator
    names = list(map(operator.itemgetter("name"), records))
Seems a bit unidiomatic given that list comprehensions are in the language... but probably many disagree.


A simple list comprehension is quite readable and better IMHO. It is very common and most people will understand directly what it does.

Of course, when you have multiple levels or complex lambdas inside, then I agree that the for loop might be preferable.


Great example. My personal preference is the first one, but I put that down to me being inexperienced and not working in a professional development environment.

I've been using Ruff with most of the rules enabled, and they have a page for this scenario here: https://docs.astral.sh/ruff/rules/manual-list-comprehension/

Another one that trips me up is the ternary operator: https://docs.astral.sh/ruff/rules/if-else-block-instead-of-i... I prefer the longer, more verbose version, even though the suggestion looks more clever.


Let me see if I can sway you.

The nice thing about both the ternary and the list compression is that they become statements of the form derived_thing = some_computation. The code flows better when you’re skimming it at a high level and thinking “then we get this”, “then pluck this”. You can think more about your reformed data and less about how it was reformed.

The alternative is that the branching obscures what you’re trying to create at each step.

Sometimes I’ll even use 2 list comprehensions instead of a loop (even though it’s slower) because it’s clearer to read something like:

    odds = […]
    evens = […]
That’s my experience, anyway.


This is my choice. So easy to get lost in the sea of intermediate veriables inside a loop. If stuff can be rewriten in list comprehensive separately then I will do it.


I’m convinced that the only reason the operator library exists is to provide inputs for functools, which itself is mostly there to pretend that you’re a functional programmer.

…but I do kinda like map().


I'd say it also heavily depends on the code style that's internal to the project. Case in point, for heavy toolz users:

    list(toolz.pluck("name", records))


list-map-itemgetter is actually the fastest solution in Python. Should actually promote this.


So I’m going to play the devil’s advocate and say that concise code has a readability advantage in that you don’t need to keep track of intermediate variables or other state across hundreds of lines of code or multiple files.

This was/is the promise of languages like APL; those willing to invest in learning arcane and terse symbols can move mountains in a few keystrokes.

I know that for my part, when reading a language I’m familiar with, it’s usually much faster to puzzle out a concise solution than a verbose-in-the-name-of-simplicity one.


That's what I like about Go. It's easy to read and understand Go code from open source projects written in it (although Kubernetes is a notable exception, with a very convoluted Go code).


I’ve been a C++ dev for a couple of decades and know my fair share of unreadable code. I’ve recently started learning Python and holy shit, it’s like you get accolades in this language for doing as much as possible in as few characters as possible. Guess I’m getting too old for these young whippersnappers.


If you think Python is like that, I advise you to never look at Ruby codebase.

But seriously, of course you can write Python one-liners or nested comprehensions, but I get the idea that it's not really Pythonic. They still want clear, iterative code. It's just more concise, but the idea is the same, but with less scrolling.


> If you think Python is like that, I advise you to never look at Ruby codebase.

Like any other language you can write perfectly expressive and intelligible code in Ruby, and in fact it is quite common to do so. The first layer of a Ruby codebase tends to be approachable, it's only when you get into some of the bigger libraries that you get some tricky metaprogramming.

Even then I don't often see Rubyists write tricky/clever code for the sake of it. The metaprogramming usually has utility and is often the "right" way to do something, especially in say the Rails codebase.

Ruby is all about developer happiness. It's the founding principle and the guiding ethos of the community.


I don't think they're talking about Ruby code golf.

Another way of stating the GP's comment is that a Rubyist looking at a Python codebase will be shocked at all of the verbose boilerplate. Ditto for a Pythonista looking at C++ code.


    If you think Python is like that, I advise you to 
    never look at Ruby codebase.
The codebase for Ruby itself, or the codebase of your typical Ruby app or library?

I spent about a decade with Ruby and my general impression is that the community really moved away from overly-clever metaprogramming.

The codebase for the Ruby language itself is definitely a challenging read.


After spending six months with Python so far, the problem with the Python ecosystem is that you have a lot of really smart people (scientists, mathematicians) who are not software engineers by trade writing a lot of these libraries.

I guess it's a good problem to have, in a lot of ways, because it's also why Python has that huge science/datascience/etc ecosystem.


Everyone thinks they're writing good code. Even many experienced seniors write code that they think is good code but others loathe. Best case, they can at least solve problems without introducing new ones; though more often than not, they're still making many junior mistakes while couching them in senior terminology.

Aside: Do we really need to keep rewording this exact same essay every month since the first computer program was ever written?


Hey if no one is writing blogspam and posting it to HN, when am I gonna get to drop my hot take on what differentiates junior, mid, and senior developers? And what about my thesis that there is actually a huge difference between programmers, developers, and engineers? My identity is really wound up in all of this so it's important that I get an opportunity to put other developers down.


This one was a bit different. It also featured an extremely dangerous and incompetent manager.


Sometimes clever code creates a good abstraction. Clever code that doesn't create a suitable trade-off with complexity isn't actually clever at all.

Of course, never code golf outside actual code golfing puzzles.


This is true. That's why the software engineering recruitment process is completely broken... They select for people who revel in unnecessary complexity.

What kind of person enjoys spending their time practicing many variations of pointless complicated coding puzzles instead of working on useful side projects or learning new concepts and technologies?

The big tech recruitment process is all about puzzle solving under time constraints though. Pragmatic engineers have no chance of competing against the puzzle solvers.


I wonder how old this advice is. I know that it predates the century but I'm not sure how far back.

(A favorite quote of mine that I believe is from the 1970s says "It's easier to make working code fast then to make fast code work."

Since the fundamental problems of programming have not changed over the centuries, I wouldn't at all be surprised if there's an anti-clever saying from the dawn of computing)


> the fundamental problems of programming have not changed over the centuries

It hasn't been a whole century yet. Grace Hopper's career started in 1944, eighty years ago.


Ada Lovelace predates her


Although it is very common for laypeople to be told that Ada is the "first programmer" in some sense, I don't find that very convincing at all. For Ada this was purely an intellectual exercise.

Grace's machine really existed, and her insight really works, even if today we would not call what she initially built a "compiler". The machine processes data, the program is data, have the machine process the program. This is recursively meta-applicable, and it's notable that rather than being something higher ups or intellectuals had specified should be done, Grace was being entirely practical.

When you write some typescript inside a C# program today and then somehow in a web browser the equivalent code ends up executed by its JS engine, that's all the fruit of Grace's insight just applied over, and over, and over.


A bit of a clickbaity title, but I'll bite

I think "cleverness" is a function of your (and your team's) experience in a particular language / domain space

I worked with teams where verbose Java is considered "simple, straightforward, and easy to understand"

I also worked with teams where terse Haskell is considered "simple, straightforward, and easy to understand"

and of course these codebases look nothing alike


It's like no one in the comments here read the piece. Honestly, it's a bit like the author himself didn't.

The big thing in this article is the dangerous and incompetent manager. People like that in a company could destroy the entire thing, and yet it's sort of just mentioned in passing like "oh that's funny". It's not funny, it's absolutely terrifying!


I suppose every generation of programmers has to rediscover this and it can’t hurt to keep writing articles and books about it.

From The Elements Of Programming Style , 1978, by Kernighan and Plauger, rule one: “Write clearly – don't be too clever.”

The whole book, a short read, spells out principles and wisdom from experience that all programmers would benefit from.


This is why I don't like working with Ruby teams. Of course you can write straight-forward Ruby code and I encourage it, but Ruby does seem to attract developers who like to write "dynamic" code and use "meta-programming" (what we would call "reflection" in other languages, where it's made intentionally difficult). I think it's mostly the influence of the Rails framework that leads developers towards "magic", although it doesn't help that Ruby makes this kind of programming especially easy.


Anytime I find myself being clever, I'm reminded of this deadpan exchange.

Tyler Durden: How’s that working out for you? Narrator: What? Tyler Durden: Being clever. Narrator: … Great. Tyler Durden: Keep it up, then.


I would extend this to clever languages: "Clever languages are the worst languages you could write in". And I would put most functional programming languages in this category.


I think the obvious problem here is the manager's attitude:

While I was proud of it, there was suddenly a problem when I talked to my manager about it.

"While I understand how complex this was, when it comes to performance reviews, this code looks trivial. It looks too easy, too simple. I would recommend writing an implementation doc of this module just so we can demonstrate that this was actually quite complex."


Cleverness is found in grokking a system.

A system of simple code is more easily understood. It's easier to move and impress the system at high-level user interfaces.

At what rational scale does the system become clever? How long is a system considered clever and how often? Can you use diff trees to infer high-level logical migration and evolution patterns within the source files themselves?


I follow Einstein's metric, "make something a simple as possible, but no simpler."


There is always some part of subjectivity when evaluating complexity and clarity.

But all things being equal, this point still stands and should be not being brushed off as "skill issue".

Clarity is a difficult skill to acquire, but it pays off.


In the spirit that Programming is Theory Building, one would want to keep it simple, consistent, and easy to understand with basic constructs defined in a somewhat irreducible (single responsibility) manner.


The actual example'd make the post better, but I understand the author is not allowed to give more details on that one :(


I don't think the python example is "too clever" though, the expanded version seems more convoluted


I like how there is a constant stream of people realizing and writing blog posts about this.


You learn about this a lot in writing and humanities classes not so much in CS classes


NARUTO - Never follow Absolute Rules as Ultimate Truth Over practicality


In reality, writing a code that is easily understandable for everyone is good code. Clever code is intricate and difficult to follow by many. Coding is done for a team of people and not to show individual preceptive of a business problem/solution.


Yep. I now find it enjoyable to refactor my code to the point of making it feel obvious and straightforward. Not always easy.

For me, it helps imagining some colleague read my code and judge me. And fortunately, I work at a place where I trust people to recognize when something that looks obvious is not actually dumb. Not that I'm so great at dumb but I try at least.

Tooling that allows you to move classes, methods, rename stuff, inline or extract functions efficiently helps a lot. It pains me a bit to write this, as someone whose favorite code editor is a text editor, not an IDE.

I find there's comparable joy in writing. Rephrasing and simplifying a text to find the most efficient / obvious phrasing. I suspect writing software might affect writing in such ways, though I don't think I would be able to tell a developer's writing apart.


I've noticed I love the clever, elegant abstractions I invent, because I understand how they work and they give me a really neat and readable way to organise my code. And I hate the clever elegant abstractions others create, because I the code is so abstract I can't tell by looking at it what it does, and I don't have their mental model of the abstraction in my head.

Cleverness is fine if you can make sure the reader gets the right mental model in their head, but not if not. And because nobody cares about your documentation, your options are to either explain it to everybody (sounds stupid but can actually work -- for a while), or to make your code explain it for you. And that limits how clever and abstract it can be.


> Clever code is probably the worst code you could write

But you should write it anyways. Preferably in a situation of little consequence (school, personal projects, prototyping, etc...)

Otherwise, how do you make the difference between code that is simple and code that is just dumb?

"not clever" code typically has: copy-pasting, no abstractions, hardcoded values, global variables, cascading "if"s... And sometimes, it is the right thing to do, but good, readable code is usually a bit more clever than that.

But how do you know the right kind of cleverness? For me, the best way is to experiment. Sometimes, trying to be clever fails, sometimes, it really makes your code better, but if you don't try, your code will never get better.


The knockdown test for too clever is that the original author can't maintain or extend the code later on. You can use this phenomenon to prove to people that they were being too clever. "See? not so easy, and this is all your doing"

Beyond that there is a a tradeoff to be had between utilizing powerful and expressive language features, and alienating less proficient programmers. In industry, redundancy and parallelism (of humans) matters a lot, and so the code has to be dumbed down quite a bit. If you're working solo or with a few highly competent peers, you can afford more cleverness.


Mostly true.


Some developers like to solve problems and some like to solve puzzles.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: