Code Quality
~ 21 minute read
25 Nov 2021
Intro
Javascript is a flexible programming language. Like anything in life, this has pros and cons, so we must carefully choose where we use it and how we use it.
Where do we use it? Obviously our first thought is in web related projects, either end user facing (i.e browser) or on the server side (Node.js). We can even build CLI apps (Node.js). However, surprisingly or not, Javascript is used even in the Linux ecosystem, for example almost 50% of gnome-shell (the graphical user interface for the Gnome desktop environment) is written in Javascript… Nice!
How do we use it? Well, here is where the fun / heated debates start. Because of its “flexibility”, we can use JS with many programming paradigms, like:
Many times it happens that we mix together all of these paradigms in the same JS codebase, which can be a
powerful
tool (otherwise we would have to use specialized languages for every purpose, like Java, Scala etc.).
If we add other JS concepts into the mix (like closures, arrow functions, this), it becomes pretty clear that we will have some problems down the road, which is
what
we will discuss next.
Details
Let’s start with a simplistic code comparison between the previously mentioned paradigms. We will use Node.js code snippets.
1// procedural
2
3function sum(x, y, z) {
4 return x + y + z;
5}
6
7module.exports = {
8 sum,
9};
10
1// procedural use
2
3const { sum } = require('./...');
4
5const s = sum(1, 2, 3);
6
7console.log(s); // 6
8
9
10
1// class based
2
3class Arithmetic {
4 iV = 0;
5
6 constructor(initialValue) {
7 this.iV = initialValue;
8 }
9
10 sum(x, y, z) {
11 return this.iV + x + y + z;
12 }
13}
14
15module.exports = {
16 Arithmetic,
17};
18
1// class based use
2
3const { Arithmetic } = require('./...');
4
5const a = new Arithmetic(0);
6
7const s = a.sum(1, 2, 3);
8
9console.log(s); // 6
10
11
12
13
14
15
16
17
18
1// prototype based
2
3const arithmetic = {
4 iV: 0,
5 sum(x, y, z) {
6 return this.iV + x + y + z;
7 },
8};
9
10module.exports = {
11 arithmetic,
12};
13
1// prototype based use
2
3const { arithmetic } = require('./...');
4
5const a = Object.create(arithmetic);
6
7a.iV = 0;
8
9const s = a.sum(1, 2, 3);
10
11console.log(s); // 6
12
13
1// functional based
2
3
4function sum(iV) {
5 return function w1(x) {
6 return function w2(y) {
7 return function w3(z) {
8 return iV + x + y + z;
9 };
10 };
11 };
12}
13
14
15module.exports = {
16 sum,
17};
18
1// functional based use
2
3
4const { sum } = require('./...');
5
6
7const mySum = sum(0);
8
9
10const s = mySum(1)(2)(3);
11
12
13console.log(s); // 6
14
15
16
17
18
Looking at them separately, everything is nice and easy. But what if we have all of them in a single file? Or in a single function?! (yes, when we have a 2000-line function, which unfortunately is seen in the wild, anything can happen down there).
And to make things a bit more complex, we add arrow functions into the mix.
So we have the equivalent:
1function sum(iV) {
2 return function s1(x) {
3 return function s2(y) {
4 return function s3(z) {
5 return iV + x + y + z;
6 };
7 };
8 };
9}
10
1const sum = iV => x => y => z => iV + x + y + z;
2
3
4
5
6
7
8
9
10
Let’s see some situations where all these together might cause problems.
Confusion for newcomers
Newcomers doesn’t mean just juniors, they can be mid/senior people that just joined a team.
There is a big chance that their onboarding process will take longer than expected, because now they have to adapt to this new “mixed” environment. However, this can also be a good thing, because this is how we learn (i.e. we encounter new things), so it will really help them to have someone guide them through the (new) codebase, and explain to them why certain paradigms were chosen in certain places. This will add clarity and will encourage them to learn and use the new paradigms.
If we are talking about juniors, then the above explanation process is mandatory. Juniors usually tend to learn procedural (which is good, I guess) and class-based (which is bad, because JS in principle has nothing to do with classes). So if they encounter these new “strange” looking things, some of them might reject them and just “stick to what they already know”.
Even mid/seniors can fall into traps. You know what they say: “if it ain’t broke, don’t fix it”, but guess what, things break, and when they break we want to be able to fix it fast. So when we encounter “mixed” paradigm code, we will spend too much effort to understand the code, then we will want to refactor it so that it “looks better”, but we don’t have enough time because the manager wants the bug fixed ASAP, then we get frustrated… You know how it goes…
Debugging is more difficult
Breakpoints
There are certain times when we just want to do an “oldies but goldies” debug session, meaning putting breakpoints, going through the code line by line, nice and easy… No “console.log()” is allowed, please.
Well, at that moment we will start to appreciate all those linting rules that we never wanted to use because “why put curly braces when they’re not necessary?”. For example:
1// put breakpoints wherever you want
2
3function sum(iV) {
4 return function s1(x) {
5 return function s2(y) {
6 return function s3(z) {
7 return iV + x + y + z;
8 };
9 };
10 };
11}
12
1// good luck putting breakpoints
2
3const sum = iV => x => y => z => iV + x + y + z;
4
5
6
7
8
9
10
11
12
Navigation
Another situation is when we nicely navigate the code with “Ctrl + click”, when all of a sudden, we encounter some code where this doesn’t work:
1// procedural
2
3const r1 = f1(0); // ctrl+click works
4
5
6
7
8
1// class
2
3const { ABC } = require('./...');
4
5const a = new ABC();
6
7const r1 = a.f1(0); // ctrl+click works
8
1// prototype
2
3const b = {
4 f1(x) {},
5};
6
7const a = Object.create(b);
8
9const r1 = a.f1(0); // ctrl+click doesn’t work (in VSCode)
10
1// functional
2
3const r = f(1)(2)(3);
4// ctrl+click works for f,
5// but we can’t go straight to f1(2) or f2(3).
6
7
8
9
10
Problems with currying
Definitions
Let’s say we decided to spice out our life and use some functional concepts in our JS code. Some concepts that I’ve seen in the wild are “function currying” and “partial application”. They are different concepts, so we should not mistake one for the other.
According to Wikipedia:
Function currying is the technique of converting a function that takes multiple arguments into a sequence of functions that each takes a single argument.
1// we want to transform f(x, y, z) into g(x)(y)(z)
2
3function f(x, y, z) {
4 return x + y + z;
5}
6
7function curry(myFunc) {
8 return function w1(x) { // w = wrapper
9 return function w2(y) {
10 return function w3(z) {
11 return myFunc(x, y, z);
12 };
13 };
14 };
15}
16
17const g = curry(f);
18
19// OR defining it directly
20
21function h(x) {
22 return function w2(y) { // w = wrapper
23 return function w3(z) {
24 return f(x, y, z);
25 };
26 };
27};
28
29console.log(f(1, 2, 3)); // 6
30
31console.log(g(1)(2)(3)); // 6
32
33console.log(h(1)(2)(3)); // 6
34
Partial application refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.
1// we want to transform f(x, y, z) into g(y, z)
2
3function f(x, y, z) {
4 return x + y + z;
5}
6
7function partialApply(myF, x) {
8 return function w(y, z) {
9 return myF(x, y, z);
10 };
11}
12
13const g = partialApply(f, 1);
14
15// OR defining it directly
16
17function plus1(y, z) {
18 return f(1, y, z);
19}
20
21console.log(f(1, 2, 3)); // 6
22
23console.log(g(2, 3)); // 6
24
25console.log(plus1(2, 3)); // 6
26
Ok, so let’s discuss each of them.
Function currying
Currying is really helpful for things like composition and partial application.
For composition it works great because curried functions are unary functions (i.e. they take only 1 parameter, and return a function that again takes only 1 parameter and so on…).
However, it can happen that we define all these nice curried unary functions, but we never use them in a composition, meaning we never use them for their intended purpose, but instead just end up calling them separately. So when we read the code, that currying will just add an extra layer of difficulty which is not really needed.
Partial application
For partial application, currying also makes sense, because what we’re actually doing is fix some parameters values, or “bind” them if you will. For example:
1function f(x, y, z) {
2 return x + y + z;
3}
4
5function g(x) {
6 return function w2(y) {
7 return function w3(z) {
8 return f(x, y, z);
9 };
10 };
11};
12
13const h = g(1)(2); // binding the params to values 1, 2.
14
15console.log(f(1, 2, 3)); // 6
16
17console.log(h(3)); // 6
18
But we can easily accomplish the same thing with JS “bind” function, so we don’t actually need currying. See:
1function f(x, y, z) {
2 return x + y + z;
3}
4
5const h = f.bind(null, 1, 2);
6
7console.log(f(1, 2, 3)); // 6
8
9console.log(h(3)); // 6
10
Also, one of the bigger problems from a code readability perspective, is when we see something like “f(x);”. It is not clear if:
There is a proposal that if accepted, it will introduce a new syntax for partial application, which will help us in dealing better with these kinds of situations.
I know the next part is not related to readability, but I’ll mention it anyway.
At the end of the day, (almost) everything in JS (we exclude primitives) is an object, so functions are objects,
arrays are objects, objects are objects etc... And we will end up with passing functions around as params.
Because
JS has the mechanism of closures, we should be careful with how we write our currying (or in general, functions
that
return functions), because there might be some performance problems (at one point I had to fix a memory leak that was caused by an
unwanted
closure).
Code review on many programming languages
This is more of a code readability thing rather than mixing programming paradigms, but I thought of mentioning it since it is something that happens frequently.
Many times a developer is part of a team which owns multiple projects, which can be written in multiple programming languages. This is a challenge itself, because we need to learn those programming languages. However, most of the time we can get away with learning just some aspects of those languages, we don’t really need to be experts in all of them (even though this would be so nice…).
Another challenge with multiple projects is that we need to do a lot of code reviews, and if we add multiple programming languages into the mix, it would really help if those projects were written in a similar fashion, so that we spend our effort on the main logic of the program, and not in some particularities of a programming language.
We can give many examples here, but I’ll just show you my favourite: using the “function” keyword.
Let’s look at some examples with Python, Golang and JS:
1# python
2
3def sum(x, y, z):
4return x + y + z
5
6a = sum(1, 2, 3)
7
8print(a)
9
1// golang
2
3package main
4
5import ("fmt")
6
7func sum(x int, y int, z int) int {
8 return x + y + z
9}
10
11var a int = sum(1, 2, 3)
12
13func main() {
14 fmt.Printf("%d", a)
15}
16
1// JS
2
3function sum(x, y, z) {
4 return x + y + z;
5}
6
7const a = sum(1, 2, 3);
8
9console.log(a);
10
If we quickly scan through this code, we will see the following:
Everything is nice and intuitive.
The JS part can also be written with arrow functions:
1// JS
2
3const sum = (x, y, z) => {
4 return x + y + z;
5};
6
7const a = sum(1, 2, 3);
8
9console.log(a);
10
Now the function declarations become:
See where I’m getting at …?
When I see “const sum = …” I intuitively think of a variable declaration, not a function. Ok, you will counter
me
and say “But you can see the parentheses after =, meaning const sum = (...”.
Yes, that’s true, but because arrow functions can have many forms, we might also have const sum = x => … , so we need to process more information.
Furthermore, I start wondering “Why do we use an arrow function? Are we trying to bypass the regular this mechanism or what’s the deal here?”.
Don’t get me wrong, I have nothing against arrow functions, I use them myself a lot. It’s just that I use them in some contexts, not all over the place.
Another one of my favourite examples is “Use better naming of variables or functions (const u = … VS const activeUsers = …)”, but this is for another time.
The main point I’m trying to make here is that all these little things add up. And I didn’t even mention all the
other stuff that a software developer needs to know nowadays (devops, security, databases, cloud, etc…), all of
which come with their own language, quirks and so on.
Everybody's free to choose what they want, but if we spend some time choosing in a smart way, that will bring us
way
more benefits in the long run.
Conclusions
Mixing paradigms is not a bad thing, sometimes it is even necessary. But we can make some choices that will make our life easier. Here are some of which we discussed:
Written by
Senior Software Engineer
Share this article on