article cover

Code Quality

Code Readability - Mixing Programming Paradigms in Javascript

~ 21 minute read

Mihai Cicioc
Senior Software Engineer

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:

  • Procedural
  • Object oriented, with it’s 2 flavors:
    • Class based (what we commonly refer to as OOP, even though that’s not 100% accurate)
      • JS class paradigm is implemented via functions and some tweaks in the prototype chain.
    • Prototype based
      • The main paradigm of JS.
  • Functional

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:

  • the invocation is a partial application.
  • it’s the final execution of the function.

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:

  • Function declarations:
    • def sum(...
    • func sum(...
    • function sum(...
  • Variable declarations:
    • a = …
    • var a = …
    • const a = …

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:

  • def sum(...
  • func sum(...
  • const sum = …

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:

  • Try to avoid mixing paradigms in the same file or even in the same function.
  • When you have newcomers in the team, spend some time with them and explain why you chose a certain thing in a certain place.
  • Try to make navigation and debugging as easy as possible, you never know when that critical production bug will pop up (using a linter is helpful).
  • Use currying for specific reasons, not just because “it is cool”.
  • Learn the existing capabilities of JS, in our situation use “bind” if you can, and if not then ok, use partial application with currying.
  • Keep code reviews in mind, there are people that have to do reviews every day on 10 projects in different languages, so help them by writing your code in a consistent and intuitive manner, and they will help you back with good code reviews that focus on logic, and not on quirks.

Written by

Mihai Cicioc

Senior Software Engineer

Share this article on

We create value through AI and digital software engineering in a culture of empowerment that enables new perspective and innovation.

OFFICE

24 George Barițiu street, 400027, Cluj-Napoca, Cluj, Romania

39 Sfântul Andrei street, Palas Campus, B1 block, 5th floor, 700032, Iasi, Iasi, Romania