I realise you might be being lighthearted, but just to be boring I’ll thrown in “The fastest and most bug free line of code in any language is the line you don’t write”.
> It is possible to write simple and obvious code in C++. A lot of people just choose not to. ;-)
You still need to be C++ fluent enough to be able to read other people's code. And it's not like everybody has the same definition of "simple and obvious".
> And it's not like everybody has the same definition of "simple and obvious".
That is a very good point. I had an interesting discussion with a Perl programmer once regarding the use of Perl's default variable $_ - to him, code that relied heavily on it was straightforward and readable. But to a newcomer, it can be very tricky to figure out what is going on. And even to an experienced Perl programmer, it can increase the mental overhead of parsing code significantly if s/he is not used to it.
For better or worse, languages that put am emphasis on readable code tend to remain not-so-popular. Python is an example, but Ada or Pascal are relatively unpopular these days.
But that is one thing I like about Go, because "idiomatic" Go code tends to be very straightforward and readable.
I mean C++ isn't going to disappear anytime too soon, but I like where we're headed with language design. Python isn't just popular, it's monumentally popular, and I think that's a great sign. In my eyes, Ada and Pascal aren't missed, precisely because there are better choices for new projects.
I love what they've done with Go from my experience with it so far, they hit the target on a number of design fronts.
The competent programmer is fully aware of the strictly limited size of his own skull; therefore he approaches the programming task in full humility, and among other things he avoids clever tricks like the plague.
Can you please explain this? It makes no sense to me (as c# developer) that `["1", "2", "3"].map(parseInt)` whould get different result from `["1", "2", "3"].map(a => parseInt(a))`.
I would argue this particular example has more to do with the radix parameter of parseInt being optional and having a complex behavior.
More generally, javascript functions accepting any number of parameters, regardless of those specified in the function declaration, is quite error prone when passing functions around.
I always use anonymous functions or 'bind' to explicitly match the parameters unless all functions involved are curried.
The other reason to avoid passing 'naked' functions around too happily is the behavior of this.
I also find most optimizations to focus on simple, explicit code. Nothing like using the less common, more dynamic features of the language to hit deoptimizations.
I wouldn't call it "hidden". If someone expected that to work, or couldn't figure out what the article example was doing, they just haven't read the documentation for 2 common functions. In fact, last I checked, always using the second parameter of parseInt was considered good practice.
There are generally two schools of thought: one is that the programmer should know better, memorize the documention and specs. The other one is that if it violates the principle of least astonishment it's just badly designed. Personally I believe that the truth is somewhere in the middle. It's good to know one's tools (including docs and specs) but I wouldn't call this kind of "clever" code maintainable.
Take care with this technique. We know that `Array.prototype.map` passes not one but three parameters to the callback function: the array element, the array index and the array itself. As a result of this,
addressParts.map((str, i, arr) => str.trim(i, arr))
Fortunately for us, `String.prototype.trim` ignores those additional parameters and only uses `this`. But if it didn't, you could end up with some head-scratching behaviour.
It might be clever, and it's the sort of thing that would be pretty cool to include in a CS lecture about higher-order functions, but it won't earn you the gratitude of fellow team members who have to maintain your code after you leave.
This is true, but including code like this should make it easier to know you should rewrite if you are going to use it in something important. I have been bitten more often by quick hack code that looks OK on the surface than code that is full of clever code like this.
I could _maybe_ see this code ending up in production code where performance optimization matters (if it actually fixes a performance issue). But if you did that you had better put a comment next to it clearly explaining why you had to do that for optimization purposes and how it works.
Avoiding creating new functions or objects is a good optimization strategy, but there's most likely very little gain as the optimizer will most likely inline the functions.
Always do the super naive, easier to understand version first, and only optimize after you have identified the bottleneck and measured the performance!
and then replace implicit this with the first argument? It is a tiny but more verbose but gets rid of all the confusion around implicit this rules so it seems like a win.
in both Chrome (62) and Firefox (58). The c-style for is very lightly faster in FF (not Chrome) but it's 767 ops/s to 725 ops/s so not exactly brain-breaking (chrome is at 454 and 452).
Safari (11) is the only one where map is a fair bit slower than c-style (420 to 377).
In all cases, both the clever map and for..of are slower than the naive map, though the magnitude varies Chrome yields very little difference but Firefox and Safari dislike for..of and like "clever map" even less.
Kind of hackish, but you can open it up in a browser and look at the console for the run times.
Basically, the clever code is generally worse, and the arrow versus for loop in Firefox and Safari are similar. But in Chrome, the for loop is quicker; it was far more noticeable in node than in chrome. One quirk of chrome is that the first time the for loop is run, it seems to take longer than subsequent runs and I am not sure why that is. I tried moving around initialization stuff and it did not seem to make any differences.
I don't see how the "sarcasm" (I use the term loosely) is justified.
You indeed failed to consider all those things, and all those things are things one should have in mind, whether a particular built-in function is faster than a custom implementation or not.
Not to mention that I said "probably optimized C++" -- I didn't claim straight out that this is the case. The other two claims still hold: it's built in, so you don't need to roll your own regex based one (complete with bugs the first time for most people), and it conveys the intention to trim perfectly.
That's even better than being faster, unless there's a real need to have that particular operation be fast in your code. Heck, the fact that you got them wrong in your initial regexes (not capturing what trim does in general) shouldn't give you enough pause? If you have gone with those BS regexes instead of trim you would have introduced a bug in the codebase.
If one wants to optimize some such method, running on a tight loop, and on a verified hot spot of their code, they would of course need to measure to see which method is faster, not just rely on someone's suggestions. That's computer science 101.
That it doesn't work "on your machine" (and on whatever browser you've checked) doesn't mean anything. One would need to check on various browsers, teat what browsers they want to target (e.g. perhaps their app is Electron and only needs to work there), and which method is faster there.
For me, the "clever" one is slightly faster in Chrome while the supposedly naive one is significantly faster in Firefox.
Looks like the naive approach not only wins in terms of readability, but performance, too. (Unless I seriously screwed this up which is always a possibility with micro benchmarks.)
This is a great example of why you should avoid writing clever code. A clever one-line piece of code requires a blog post to make sense of it.
Not slating the article. I thought it interesting and the author does mention this caveat, making a point of saying the typical way is honestly more readable.
As for the project they found this in, I hope this stuff isn't thread through it, for the maintainer's sake and sanity.
66 comments
[ 2.7 ms ] story [ 134 ms ] thread... still nice article... ;)
You still need to be C++ fluent enough to be able to read other people's code. And it's not like everybody has the same definition of "simple and obvious".
No, but it's like we can expect of people to more or less agree on a definition of "simple and obvious" or else be outliers.
That is a very good point. I had an interesting discussion with a Perl programmer once regarding the use of Perl's default variable $_ - to him, code that relied heavily on it was straightforward and readable. But to a newcomer, it can be very tricky to figure out what is going on. And even to an experienced Perl programmer, it can increase the mental overhead of parsing code significantly if s/he is not used to it.
But that is one thing I like about Go, because "idiomatic" Go code tends to be very straightforward and readable.
I love what they've done with Go from my experience with it so far, they hit the target on a number of design fronts.
Dijkstra (1972) The Humble Programmer (EWD340).
Map calls a function with "currentValue", "index", and "array".
So the calls essentially look like this:
Why are they not equivalent?
EDIT: Never mind, another comment explained this.
More generally, javascript functions accepting any number of parameters, regardless of those specified in the function declaration, is quite error prone when passing functions around.
I always use anonymous functions or 'bind' to explicitly match the parameters unless all functions involved are curried.
The other reason to avoid passing 'naked' functions around too happily is the behavior of this.
I also find most optimizations to focus on simple, explicit code. Nothing like using the less common, more dynamic features of the language to hit deoptimizations.
(As long as no one actually asks these questions in interviews)
Forget about String.prototype.trim at the moment. Consider this function:
This function prints the receiver (aka "this"), and arguments.If you use it in the following way:
The output is Meaning, that the receiver (aka "this") of the function will be the array element, and the arguments will be the array index and the array itself.It might be clever, and it's the sort of thing that would be pretty cool to include in a CS lecture about higher-order functions, but it won't earn you the gratitude of fellow team members who have to maintain your code after you leave.
I'll take 10 lines of code that clearly self documents with elaborately named variables over this 1 line of magic.
Good code is straight forward, carries no surprises and makes the whole task at hand look easy.
Clever =/= robust or reliable, many times, and that's what they pay us the bucks for.
[1] https://zpao.com/posts/calling-an-array-of-functions-in-java...
Avoiding creating new functions or objects is a good optimization strategy, but there's most likely very little gain as the optimizer will most likely inline the functions.
Always do the super naive, easier to understand version first, and only optimize after you have identified the bottleneck and measured the performance!
1) It is less idiomatic
2) Does not provide a performance gain
1) Accurate
2) Succinct
Personally I don't find the results convincing. With 20k iterations and 18 trials I have to squint to see a practical difference. But please, judge for yourselves. Numbers: https://docs.google.com/spreadsheets/d/1mZx7ENLTeCG0G_AwjCCv...
Safari (11) is the only one where map is a fair bit slower than c-style (420 to 377).
In all cases, both the clever map and for..of are slower than the naive map, though the magnitude varies Chrome yields very little difference but Firefox and Safari dislike for..of and like "clever map" even less.
Kind of hackish, but you can open it up in a browser and look at the console for the run times.
Basically, the clever code is generally worse, and the arrow versus for loop in Firefox and Safari are similar. But in Chrome, the for loop is quicker; it was far more noticeable in node than in chrome. One quirk of chrome is that the first time the for loop is run, it seems to take longer than subsequent runs and I am not sure why that is. I tried moving around initialization stuff and it did not seem to make any differences.
Times from node:
Basically, code in the comfortable style. I used to love for loops, but have now switched over to map and forEach when it makes sense to do so.Considering that it's built in, probably optimized C++, and does just what it says on the tin, then yes.
But you're right, I failed to consider that it's "probably optimized". And "C++". /s
You indeed failed to consider all those things, and all those things are things one should have in mind, whether a particular built-in function is faster than a custom implementation or not.
Not to mention that I said "probably optimized C++" -- I didn't claim straight out that this is the case. The other two claims still hold: it's built in, so you don't need to roll your own regex based one (complete with bugs the first time for most people), and it conveys the intention to trim perfectly.
That's even better than being faster, unless there's a real need to have that particular operation be fast in your code. Heck, the fact that you got them wrong in your initial regexes (not capturing what trim does in general) shouldn't give you enough pause? If you have gone with those BS regexes instead of trim you would have introduced a bug in the codebase.
If one wants to optimize some such method, running on a tight loop, and on a verified hot spot of their code, they would of course need to measure to see which method is faster, not just rely on someone's suggestions. That's computer science 101.
That it doesn't work "on your machine" (and on whatever browser you've checked) doesn't mean anything. One would need to check on various browsers, teat what browsers they want to target (e.g. perhaps their app is Electron and only needs to work there), and which method is faster there.
https://jsperf.com/trimcall/1
For me, the "clever" one is slightly faster in Chrome while the supposedly naive one is significantly faster in Firefox.
Looks like the naive approach not only wins in terms of readability, but performance, too. (Unless I seriously screwed this up which is always a possibility with micro benchmarks.)
Node v9.1 + benchmark.js clever-solution x 249,543 ops/sec ±0.36% standard-arrow-function x 213,926 ops/sec ±0.35% old-for-loop x 261,449 ops/sec ±0.54%
Not slating the article. I thought it interesting and the author does mention this caveat, making a point of saying the typical way is honestly more readable.
As for the project they found this in, I hope this stuff isn't thread through it, for the maintainer's sake and sanity.