Senior Software Engineer
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.
Let’s start with a simplistic code comparison between the previously mentioned paradigms. We will use Node.js code snippets.
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:
Let’s see some situations where all these together might cause problems.
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…
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:
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:
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.
Partial application refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.
Ok, so let’s discuss each of them.
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.
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:
But we can easily accomplish the same thing with JS “bind” function, so we don’t actually need currying. See:
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).
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:
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:
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.
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: