A Clever Line of JavaScript
blog.bloomca.me
blog.bloomca.me
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.
addressParts.map(Function.prototype.call, String.prototype.trim)
is roughly equivalent to 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.Forget about String.prototype.trim at the moment. Consider this function:
const f = function() { console.log('receiver is', this, '. arguments are', arguments) }
This function prints the receiver (aka "this"), and arguments.If you use it in the following way:
['a', 'b', 'c'].map(Function.prototype.call, f)
The output is receiver is [String: 'a'] . arguments are { '0': 0, '1': [ 'a', 'b', 'c' ] }
receiver is [String: 'b'] . arguments are { '0': 1, '1': [ 'a', 'b', 'c' ] }
receiver is [String: 'c'] . arguments are { '0': 2, '1': [ 'a', 'b', 'c' ] }
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.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", "2", "3"].map(parseInt)
> [1, NaN, NaN]Map calls a function with "currentValue", "index", and "array".
So the calls essentially look like this:
parseInt("1", 0, arrayReference) // => 1
parseInt("2", 1, arrayReference) // => NaN
parseInt("3", 2, arrayReference) // => NaNWhy 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.
["1", "2", "3"].map(_.bind(parseInt, null, _, 10));
=> [1, 2, 3] > '\t1 ;2;3;4;5;6;7;8;9\r\n;10\n'.split(';').map(Function.prototype.call, String.prototype.trim)
[ '1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ]
> '\t1 ;2;3;4;5;6;7;8;9\r\n;10\n'.match(/[^;\s]+/g)
[ '1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ]
> '\t1 ;2;3;4;5;6;7;8;9\r\n;10\n'.match(/\d+/g) // this ones' a bit different
[ '1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ]
EDIT: or even (I'm still thinking here) > '\t1 ;2;3;4;5;6;7;8;9\r\n;10\n'.trim().split(/[;\s]+/)
[ '1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ] > '\t1 ;2 a;3;4;5;6;7;8;9\r\n;10\n'.split(';').map(Function.prototype.call, String.prototype.trim)
[ '1', '2 a', '3', '4', '5', '6', '7', '8', '9', '10' ]
> '\t1 ;2 a;3;4;5;6;7;8;9\r\n;10\n'.match(/[^;\s]+/g)
[ '1', '2', 'a', '3', '4', '5', '6', '7', '8', '9', '10' ]
> '\t1 ;2 a;3;4;5;6;7;8;9\r\n;10\n'.match(/\d+/g)
[ '1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ]
Edit (re. your edit): > '\t1 ;2 a;3;4;5;6;7;8;9\r\n;10\n'.trim().split(/[;\s]+/)
[ '1', '2', 'a', '3', '4', '5', '6', '7', '8', '9', '10' ]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.
... 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).
(As long as no one actually asks these questions in interviews)
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...
let trimmed = strings.map(str => str.trim());
and a c-style for let trimmed = [];
for(var i=0; i<strings.length; ++i) {
trimmed.push(strings[i].trim());
}
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.
Times from node:
Prototype: 105.467ms
Arrow function: 106.674ms
For: 98.820ms
Prototype: 105.743ms
Arrow function: 106.192ms
For: 82.733ms
Prototype: 105.512ms
Arrow function: 106.517ms
For: 82.625ms
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. functions.forEach(Function.prototype.call, Function.prototype.call)
this is approximately equivalent to functions.forEach(f => f())
although in practice it's actually more like functions.forEach((f, i, arr) => f.bind(i)(arr))
so let's hope `f` isn't too sensitive to the value of `this` or that `arr` parameter...[1] https://zpao.com/posts/calling-an-array-of-functions-in-java...
1) It is less idiomatic
2) Does not provide a performance gain
1) Accurate
2) Succinct
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.
f(this, other) -> f.call(this, other)
obj.f(other) -> obj.f.call(obj, other)
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.