Clever code is probably the worst code you could write (2023)
read.engineerscodex.com
read.engineerscodex.com
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 GOTOWhat 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.
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.
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.
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.
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.
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!
Oh I love that blog series.
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.
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.
> 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
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'.
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.
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.
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.
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.
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.
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.
It's also the facade design pattern.
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.
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.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.
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 exceptionWhere 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.As to your example, both LBYL and EAFP are accepted Python standards.
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.
- 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)
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
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]
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. 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. 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.It's a matter of preference, but I'd still risk ambiguity and name collision, especially when you go beyond std:: (like boost stuff).
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.
https://stackoverflow.com/questions/9981607/array-foreach-ru...
We tested the normal loop vs. the clever loop. Performance-wise, the normal loop blew the doors off the clever loop.
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.
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.
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.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.
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.
Why would I want to add by default?
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 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.
Accumulate, surprisingly enough, accumulates by default:
return accumulate(x.begin(), x.end(), 0); std::accumulate(v.cbegin(), v.cend(), decltype(*v.cbegin()){});
:) :) :) #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;
}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).
(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;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.
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.
In C# it is just
return numbers.Sum();In Rust you would probably just write: x.iter().sum()
Only collections of numeric types (including arrays) have Sum.
https://learn.microsoft.com/en-us/dotnet/api/system.linq.enu...
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...
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.
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.
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)
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.
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.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...
The one-liner approach tends to include an explicit type declaration (Or turbofish), `iter()`, and `collect()`, at minimum.
x.iter().sum()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.
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]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.
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.
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.
x = np.arange(0,10,1)
print(x[x%2==0])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.
(0..10).select{it.even?} 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.
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."
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.
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.
list.iter().sum()
ezpzC++ 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.
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 :)
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.
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.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.
(Heh, even Malbolge Unshackled is likely Turing-complete.)
fold_left(x, 0, _1 + _2)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?".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)
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.
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.
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.
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.
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" 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.
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.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.
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.Of course, when you have multiple levels or complex lambdas inside, then I agree that the for loop might be preferable.
list(toolz.pluck("name", records))…but I do kinda like map().
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.
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.
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.
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.
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.
Aside: Do we really need to keep rewording this exact same essay every month since the first computer program was ever written?
Of course, never code golf outside actual code golfing puzzles.
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.
(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)
It hasn't been a whole century yet. Grace Hopper's career started in 1944, eighty years ago.
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.
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
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!
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.
Tyler Durden: How’s that working out for you? Narrator: What? Tyler Durden: Being clever. Narrator: … Great. Tyler Durden: Keep it up, then.
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."
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?
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.
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.
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.
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.
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.