DEV Community

Cover image for Why do we write JavaScript like this?

Why do we write JavaScript like this?

Anders on August 21, 2020

Before we start, I do realize that this may potentially come off as a little harsh or negative, but that is not my intent, I am just curious as to ...
Collapse
 
tiagojpdias profile image
Tiago Dias

I don't want to sound bitter but...

I don't get the idea of writing convoluted examples to force your point of view...

How is this hard to read?

const isPalindrome = (actualString) => {
  const reversedString = [...actualString].reverse().join('');

  return actualString === reversedString;
}
Enter fullscreen mode Exit fullscreen mode

And why would you rather write code like this?

function isPalindrome(s) {

  for (var i = 0; i < s.length / 2; ++i) {

    if (s[i] != s[s.length - 1 - i]) {

      return false;
    }
  }

  return true;
}
Enter fullscreen mode Exit fullscreen mode

Single character variables? s.length -1 -i ? Creating if statements to return true/false ?

How s this more readable to you?

Collapse
 
anders profile image
Anders

This is exactly the discussion I want us to have =)

as for the particular example you highlight (and I do take your point about "s", although "i" as a common name for an iterator.. well, its quite common)

In my view what you can see there is code that more explicitly mirror the actual functions "action", specifically it explicitly does what you'd do to see if a string is a palindrome or not, ie, you check the "mirror" character to equal the current character.

Using an if statement for an early out I feel also is quite handy, as soon as its apparent that its not a palindrome, we return, without having to process the entire string in those cases.

Collapse
 
functional_js profile image
Functional Javascript

Choosing implementation variants is straightforward if you adhere to a predefined criteria set.

I write about how to evaluate implementations on criteria here:
TLDR: Choose the fastest implementation considering equal robustness.
dev.to/functional_js/squeezing-out...

In this case, the loop is many magnitudes faster. Use the loop.

//a.
const isPalindrome_loop = str => {
  const s = str.toLowerCase();
  for (let i = 0; i < s.length / 2; ++i) {
    if (s[i] !== s[s.length - 1 - i]) {
      return false;
    }
  }
  return true;
};

//b.
const isPalindrome_spread = str => {
  const s = str.toLowerCase();
  return [...s].reverse().join("") === s;
};

//@tests
const palid = "A man, a plan, a canal: Panama—!@#$#@%$$%^&&^*()=-_+,.<>/?";

//a.
timeInLoop("isPalindrome_loop", 1e6, () => isPalindrome_loop(palid));
// isPalindrome_loop: 1e+6: 162.986ms

//b.
timeInLoop("isPalindrome_spread", 1e6, () => isPalindrome_spread(palid));
//isPalindrome_spread: 1e+6: 5.494s
Thread Thread
 
anders profile image
Anders

Very clear comparison of the actual performance of the two.

I do wonder though why declaring the actual functions as "function Name(arg) { ..." has fallen somewhat out of favor, it just seems such a more natural way to do it given how the English language works.

Then again in most other languages you'd typically see:
returnType FunctionName(args) {
or something similar to that

Thread Thread
 
functional_js profile image
Functional Javascript

That's a good question, here's my answer to it:

I don't understand why people still use the "function" keyword, unless you are accessing the "this" keyword, which should be never, except for legacy JavaScript.

Plus I would pick one consistent strategy and go with it, instead of intermingle, "sometimes arrow and sometimes function".

I also don't use single quotes. Everything double quotes.
Then there's none of this, "sometimes this, sometimes that" sprinkled all over the place.

I'm a minimalist, and only use a subset of the language.
Here is a list of what I don't use in JavaScript:
dev.to/functional_js/what-subset-o...

Thread Thread
 
anders profile image
Anders

Thanks for that response.

I agree, consistency is key. Which is why on my projects there are always a set of coding guidelines that are enforced in terms of naming, spacing, comments, all that.

I do use the "this" keyword relatively frequently however myself, specifically from within member functions of an object, to access member data or other member functions.

Collapse
 
sargalias profile image
Spyros Argalias • Edited

It's about readability and understandability.

I never aim to write functional code. I aim to write what I consider to be the best code I can write.

First example

In the palindrome example, the function body is 2 lines long:

function isPalindrome(str) {
    const reversedStr = str.split('').reverse().join('');
    return reversedStr === str;
}

The function body of the loop version has 4 lines of code and a few more lines with braces. The volume of code to read is much higher, and you must read all of it before you understand what it's doing. Furthermore, you have to actually parse the logic in your head and see how it works, which I find much more difficult than the simpler logic in the functional example.

"reverse the string and check if it's equal to the original", compared to "loop through the string, for each index, grab the current character, check if the current character is equal to the character at the index from the opposite end of the string and return false if it isn't".

It just takes longer to read and understand for someone who is well-versed in both versions.

About performance: In many apps, it brings no benefit to optimise performance for simple things (premature optimisation). It's usually better to optimise for readability and only optimise for performance if needed.

Second example

I would have written the second example like this:

import {add} from 'ramda';

const sum = (...args) => args.reduce(add, 0);

Using a library, or coding your own versions of these utilities, is fine, since you'll reuse those functions in many places throughout your codebase (DRY).

Again, if you understand what this is doing (which just takes a bit of practice), this is instant to read and understand. It says, "for every item in the list, add it to the total, with the total starting at 0".

The for-loop version is fine, but again, you have to read every word and parse it to understand it.

And so on.

Edit: Fixed the code.

Collapse
 
vonheikemen profile image
Heiker

Peer pressure? Almost everyday I read an article that has the following:

Imperative code is messy and error prone.

You should avoid mutability at all cost.

Don't use var. Don't use for loops.

Their solution? Writing in the way you describe in this article. And this is the kind of content that influence new developers. Some may end up obsessed with immutability and "being declarative" to the point they don't consider performance at all.

Collapse
 
wulymammoth profile image
David

The responses that you quoted are valid, but difficult to stand on their own. I much much prefer declarative code -- it does what it says. In the context of JavaScript, particularly in browser, the performance argument is debatable.

Any extremes without considering BOTH readability AND performance, will make someone unhappy. On the other extreme if you optimize for just performance, recursion should never be used -- everything should be written with imperative and iterative code. On this extreme, we have the competitive programmers with single name mutable variables and no spaces between operators -- compact, concise, and performant.

Cargo culting is bad if people don't change their perspectives given new information. I think it's fine that newcomers come in and stand on one side first without having to be paralyzed by having to think about both, but for an experienced dev, I'd imagine the expectation to be different and in mixed paradigm languages, like JavaScript, things aren't mutually exclusive (we can have both), but in a functional language -- ALWAYS use declarative code as the VM will optimize it anyway (e.g., tail-call optimization, and immutable data-types actually reference the same address in memory)

Collapse
 
pentacular profile image
pentacular

None of the examples shown are declarative.

I think people are somehow confusing point free code with declarative code, which is quite different.

You can put the points back in to make this obvious.

Thread Thread
 
wulymammoth profile image
David

I think you're right -- I mean the functions themselves are not, but the devil's in the details... as I would consider the use of reverse to be declarative within the body of the function.

[via Wikipedia]

In computer science, declarative programming is a programming paradigm—a style of building the structure and elements of computer programs—that expresses the logic of a computation without describing its control flow.

There is no logic expression that describes control flow in the use of the reverse method.

That said, I don't believe point-free code and declarative code are mutually exclusive -- SQL comes to mind

Thread Thread
 
pentacular profile image
pentacular

With that definition, all procedure calls are declarative, which seems incorrect to me.

A declaration describes the state of the universe, a procedure call makes things happen.

What things it makes happen might be hidden from you, but the call itself is an explicit operation at a point in time, containing those hidden operations.

Thread Thread
 
wulymammoth profile image
David • Edited

With that definition, all procedure calls are declarative, which seems incorrect to me.

I see what you're getting at. I'm really just talking specifically about the method, reverse, afforded by arrays in JavaScript. I'm not talking about the body of the function which is what I believe you to be interpreting here. This is why I agree with you that the examples used are not declarative.

However, I'm wondering how you would describe the difference between the following, because to me, the first example, in my opinion, is imperative and procedural:

(function(nums) { 
  let sum = 0;
  for (let i = 0; i < nums.length; i++) {
    sum += nums[i];
  }
  return sum;
})(numbers);

// versus
numbers.reduce((sum, num) => sum + num); 
Enter fullscreen mode Exit fullscreen mode

The second example says what we're doing rather than how we do it: "reduce the numbers to a sum total", whereas the first example's body has order dependence and is imperative that we declare and initialize s before looping across the input numbers and then adding to s...

Like, how would you describe the "sum" operation in the absence of the term, "declarative"?

A declaration describes the state of the universe, a procedure call makes things happen.

[via Wikipedia]

In computer programming, a declaration is a language construct that specifies properties of an identifier: it declares what a word (identifier) "means"

I think you're conflating declarative programming here with a declaration.

// standard declaration (uninitialized)
let foo; 
Enter fullscreen mode Exit fullscreen mode

I'm really just honing in on the paradigm itself and how operations are expressed -- functions that say what we're doing as opposed to how we do it in the spirit of how "declarative programming" is often understood and described

Collapse
 
_hhandoko profile image
Herdy Handoko • Edited

I'm not sure I understand, if this is a satire piece then I missed it completely.

All the examples provided, are far more readable than the author's own examples. Perhaps, it is because those operations are inspired by Functional Programming concepts, and has commonality across many different languages.

Putting performance aside (for now), these examples shows intent. It tells the 'what' rather than the 'how'. In one glance, I can tell what the first code is supposed to be doing, not so much with the for loop of the second one where I have to inspect and step-through the algorithm (in my head).

In my personal experience as well, for loop implementations need to be accompanied with comprehensive tests as it is easy to make a mistake in them.

Performance matters, but I do believe readability and correctness matters more.

I would have been fine if the focus of the article was simply on performance of conventional, imperative JS code. However, it should be acknowledged that readability and comprehension is way lower.

Collapse
 
anders profile image
Anders

The article posts a question "why", and I think there has been quite a few potential answers to that question.

Not sure where you see many issues with loops though, care to elaborate with examples? =)

Collapse
 
_hhandoko profile image
Herdy Handoko

Missed the reply notification 😬

Not so much of a problem, but rather that for loops carry higher cognitive overload, whereas map or filter is composable and easier to comprehend (e.g. treating it as data pipeline).

So there is definitely a tradeoff, readability vs performance.

Collapse
 
dfockler profile image
Dan Fockler

I think it comes down to the idea that the less state and logic you have to manage the less bugs you'll end up with. Now that might not be true, but intuitively it makes sense. If there's less things I have to manage in my head, the easier things are to understand and the harder they are to mess up.

In your capitalizeAllWords example the longer example has 5 variables to manage, while the functional version has 1 variables (the input string). A lot of the code in the imperative version doesn't pertain to what the function is doing (setting up the loop, setting up the intermediate variables, changing the variable state), while the functional version is almost completely just the operations you are interested in to complete the operation, and includes only one logical branch doing the input type check.

I'd argue that speed of execution doesn't really matter at the small scale, for Javascript. For things like rendering a Virtual DOM, it will dwarf the execution time compared to running a small user created function.

Collapse
 
anders profile image
Anders • Edited

Good points for sure. I'd argue the short version has two variables (sentence and word), but I take your meaning : ).

There is ofc more actual "variables" or pieces of data at least than that, just that they are never named, thus potentially they do not count.

The performance reflection is ofc also true, its more the general pattern of not caring that eventually gets us into trouble.

Collapse
 
heyitsarpit profile image
Arpit • Edited

This post just reminds me how badly we need a better standard library in JavaScript. This is what I mostly miss from python.

Collapse
 
anders profile image
Anders

As someone who has never used python, do you have any specifics to share on how something like that would look and work? : )

Collapse
 
heyitsarpit profile image
Arpit • Edited
def isPalindrome(string):
  return string == string[::-1]

A lot of this stuff regarding string and list(array) manipulation in python can be just a one liner.

Thread Thread
 
anders profile image
Anders

without deep knowledge of how python works that is not intuitive for me sadly. Might be time to look at some python stuff to get better insight =)

Thread Thread
 
heyitsarpit profile image
Arpit

That's fine. The only issue is the unfamiliarity with [::-1] or [:n] or [start:end] syntax. This works like Array.slice() for reading lists and strings. It just says you want to capture the list/string between a start and end index. The negative one means read the string backwards.

Thread Thread
 
anders profile image
Anders

Looked it up just now as well, there are certainly some powerful capabilities there, including the stride thing, real nice.

There is a lot of room for ergonomic improvements in JS. But it will forever be hampered by the "compiler" being on the remote system.

Collapse
 
eelstork profile image
Tea

Anders, I think you need to see this:
dev.to/eelstork/introducing-howl-a...
: ))
Concision is valuable on its own. When concision works against clarity, there is a problem. How do you balance these two?
Not that simple. I'm not a huge fan of lambdas, but then again, as a programmer I predate (the widespread adoption of) lambdas. So there's an issue here of:

  • "not clear" (because we are not familiar with a notation or coding style x)
  • "not clear" (after getting enough familiarity with said coding style or notation)

You are showing "not much longer" code which ends up 3x 4x longer than the alternative. Programmers spend most time reading code and navigating code bases they know.
3x shorter, I think that's valuable when scanning through thousands of lines of code.

There isn't a standard of clarity per se (well; linters, good practices and so forth, but that is definitely not the end of it). What matters is what works for you, and your team.

Now, talking performance, the answer is really simple. BEFORE benchmarking your code there should be a perceived need for the code to run faster. Optimise for readability, concision and productivity. These concerns duly override worrying about how fast your code is running.

Collapse
 
anders profile image
Anders

∘ ㅇ IsModifier(ㄹ x) {
⤴ (howlTemplate ☰ null) ⤬
⤵ ∀ (∙ k ∈ howlTemplate) ⤴ (k ☰ x) ㆑ ⤬
}

Ohh my =)

Collapse
 
tominflux profile image
Tom

Does this count as an esoteric language?

Collapse
 
moopet profile image
Ben Sinclair • Edited

I generally agree, though I think the examples you've given weren't too bad in terms of readability...

... and I'd like to point out that doing the "old fashioned" version of captilizeAllWords you've introduced a bug. Because you're not splitting on words, a double-space in the string will mess it up. That's a mistake that's harder to make using the terser format.

I do think that a lot of (especially new) Javascript coders are trying to be as terse as possible because it maybe makes them feel more advanced, or that they're doing things the "best practice" or "modern" way, but the point this post is making stands:

The style of code that you write affects how other people read and maintain it far more than it affects the application itself.

Performance is important, but premature optimisation is the cube root of all evil... and less code doesn't necessarily give more performance. Loop unrolling, for example, is a recognisable way to improve performance but it means you write more code in a messier way that future you is gonna hate.

Collapse
 
anders profile image
Anders

Quite possibly you are right there, its just "the pattern" that stands out, not necessarily that these where the worst offenders by any stretch of the imagination =)

Actually, if you do run that version of captilizeAllWords, ex: captilizeAllWords("will this fuck up?") => "Will This Fuck Up?" so it does work, unless ofc you also want it to remove any double spaces...

Your observation seems accurate to me, it seems terseness and/or cleverness is weighing too highly on the scales, especially for newer coders.

And I do as well agree that you should (mostly) not sacrifice readability for performance, but you also should not totally disregard performance, lest we all end up in the death by a thousand cuts scenario.

Collapse
 
moopet profile image
Ben Sinclair

You're right, I didn't try running it and it's obvious it'll work now I look again. But that's part of the problem, isn't it? It's not always obvious how the flow goes no matter what style you use!

Thread Thread
 
anders profile image
Anders

So true, that is our never ending challenge =)

Collapse
 
psfeng profile image
Pin-Sho Feng • Edited

You're basically comparing functional style vs imperative style. Functional style is higher level (as in C is higher level than assembly) and we often compose generic functions that can be applied in multiple situations such as reduce (used to loop through and accumulate values over collections).

It may seem less readable to the untrained eye but when you're used to it, you appreciate not having to think about the low level details, which often imply mutation, state management and so on, which with enough time will grow spaghetti and become difficult to maintain.

Collapse
 
rsa profile image
Ranieri Althoff • Edited

That is because Javascript, even though it has the concept of iterables and generators for ages, still does not use them enough.

Take Python, for instance: reversed(x) does not allocate memory if x is randomly indexable. Same for map and filter, which don't execute until you require them. That means:

const [x] = arr(x => x ** 2)

Should only execute the callback ONCE, as we are extracting just the first element. In plain JS, however, it executes for everything and returns a new array.

Collapse
 
spazmodius profile image
rajiv • Edited

Why do we right code like this?

if (s[i] != s[s.length - 1 - i]) {
Enter fullscreen mode Exit fullscreen mode

This allocates 2 1-character strings, and performs a string comparison (not a character comparison, there is no character datatype in Javascript).

If efficiency is our criteria, then we would write

if (s.charCodeAt(i) !== s.charCodeAt(s.length - 1 - i)) {
Enter fullscreen mode Exit fullscreen mode
Collapse
 
anders profile image
Anders

That is great, how does that perform compared to what I wrote?

Collapse
 
spazmodius profile image
rajiv • Edited

That is, of course, the right question.

In Node 12, it seems to make no difference at all. Maybe they optimize to the same thing.

Results may vary in other runtimes.

Collapse
 
pj profile image
Paul Johnson

I think we're at this weird time in programming where functional concepts are becoming wider spread, but still not ubiquitous enough that you can necessarily assume people will be familiar with them. It's possible that over the next 10 years FP will become the dominant programming paradigm and OOP will become legacy or restricted to certain use cases.

In that world I would expect the FP style will become familiar to a majority of programmers and so won't be strange to see it in examples.

I also I think it represents a gap between enthusiasts who produce blog posts and tend to be up on the latest code style trends and the majority of programmers who mostly work in an OOP/imperative style.

Collapse
 
scroung720 profile image
scroung720

I have little time to participate in this discussion but I have some 1 opinion, 1 free resource, and 1 paid resource:
1) For me, understanding this problem is the difference between seniors and juniors developers... Junior developers tend to think that programming is a bidimensional activity driven by Skills and Knowledge. After some experience as a programmer, you start to realize that the most important thing about creating software is the scalability and maintainability. It is more expensive to maintain code than creating it. So, anything that you can do to make maintainability easier(a.k.a. cheaper) is welcome that includes abstracting implementation details.
2)This whole video answers your question: frontendmasters.com/teachers/kyle-...
3)I recommend you take the Functional Javascript of Kyle Simpson the author of the book 'You Don't Know JavaScript', unfortunately, it is not a free resource but it is worth much more than what it cost. Highly, recommend the frontend master courses I have paid 3 months now and I feel with a higher level on my JavaScript.
frontendmasters.com/courses/functi...

 
pentacular profile image
pentacular

Well, bad poetry isn't to everyone's taste, so it's understandable.

Mostly I wanted to convey that I think that I understood their position, and that it was a fine personal choice, but might lead to a situation that they would eventually come to regret.

Without framing it as an argumentative position, which is the tricky bit.

 
johnkazer profile image
John Kazer

Makes me think of an interesting split of opinion or approach or maybe programming temperament.

Some folk focus on the logic of what they are trying to achieve, they like functions and hate pointers. Others like to know more about what the computer is actually doing, they love pointers and benchmarking.

I'm being simplistic but it made me wonder...

Collapse
 
tominflux profile image
Tom

I have never ever actually had a problem with JavaScript running too slowly. Only ever issues with performance of complex graphical operations stemming from low-level. The readability that this functional, immutable style of JS provides has, however, been incredibly beneficial to me in terms of being able to wrap my head around large codebases.

Collapse
 
johnkazer profile image
John Kazer

If concerned about performance over functional clarity should we be even using JavaScript? Rather than web assembly?

Collapse
 
anders profile image
Anders

That may certainly be an option if you do bot need to support older browsers. My focus here though was specifically JavaScript patterns. I think we can all agree that JS isn't exactly the perfect language.

Collapse
 
xowap profile image
Rémy 🤖

I draw the line at: if I have to Google how to make this code a one-liner then maybe it's not right to do.

Collapse
 
anders profile image
Anders

Hehe, that sounds like the ultimate sanity check =)