enso with his cat

JavaScript Functional programming demystified - Functions

Demystifying functional programming for JavaScript developers every day. With practical tips that you can start using today even if your team is not practicing functional programming.

Introduction

If we start with the question, what is a paradigm? To me, a paradigm is a way of doing things; it defines how we think about problems and how we approach them.

Naturally, for us, it is procedural since it feels most familiar, as it is concrete and imperative. We reason about programs like a recipe for a cake; we write concrete and super-specific steps to bake a cake. While functional is a bit different and needs us to change this habit of thinking in imperative steps, but rather we need to reason about steps in a declarative way. So functional programming is about... you would be surprised by making software with functions 😂. Fundamentally, writing software by composing pure functions and describing functions in a declarative way.

The phrase "declarative programming" means that the program logic is expressed without explicitly describing the flow control.

In the first part of this series, I will focus on functions the most.

Functions

A function is an extracted procedure into a block of code. We give that block a name so we can call it in different places and as many times as we want, with or without parameters.

We need to understand two things before proceeding. First, JavaScript treats functions as first-class citizens. Functions are values like any other value. You can assign them, pass them to another function, or return them from a function. That is one of the most essential points for the ability to utilize FP principles.

The second principle is the concept of purity. Don't get me wrong, of course, it is hard to think about purity in an unpure world, but for us to benefit from FP concepts we are interested in pure enough

Let's quickly review what a "pure" function is. First, a function must be idempotent, meaning calling it once or many times will always produce the same output.

It must not interact with the outer world —the global state— nor should it be affected by the outer world, i.e., it should not produce side effects. It must not change the arguments passed in.

Side effects

A side effect is an event caused by a system within a limited scope in which an effect falls over that scope. So, technically, even if we use console.log inside our functions, the function will be unpure. It's unpure because console.log will fall over the scope of the function it is called on, but we should not be bothered about console.log.

Let's take a look at some impure examples.

1const number = 2
2const impure = () => 4 + number
3

second one

1const impure = fruit => fruit.strain = true
2

And the next one below would be pure enough, Like I already mentioned it's not possible to be completely pure in the unpure world, and we should not be bothered with it in everyday life

1const pureRandomNumber = () => 21
2

to explain this statement, "unpure world," let's take an example: make a function that passes all of our requirements to be pure, idempotent, and will not interact with or change anything outside. Heck, it doesn't even have arguments.

1const number = pureRandomNumber()
2

require them, and we can affect the outer world and crash the application. However, we don't need to stress about these edge cases and consider these pure functions.

1const isNumber = pureRandomNumber((() => { throw 'Bumm'})())
2

When we rely on pure functions, we can achieve cool things. One of them is memoization. Since we are only dependent on inputs to generate outputs if we call the function twice with the same parameters, we can memorize the output, and on the second call, we can return the same result without extra computing.

Loops

Loops are imperative. We tell the program exactly how to increment numbers and what to do in each iteration.

1for(let i = 1; i <= 10; i++ ) {
2 console.log(i)
3}
4

If we want to immerse ourselves in the functional paradigm, a functional way to think about loops is through recursion. Recursion is a function that calls itself until specific criteria are met.

Essentially, we run some code and call in function until some criteria are met. In the example above, the criteria would be i = 10;

But recursion is abstract and at first it is really hard to understand how to do it properly but for that, I love the quote from Graham Hutton

Defining recursive functions is like riding a bicycle: it looks easy when someone else is doing it. It may seem impossible when you first try to do it yourself, but it becomes simple and natural with practice.

You just need a lot of practice to make it super easy to write them. When you finally get enough practice, it will be exactly the same as riding a bike, and you will do it without much thinking about it.

So, if we wanted the same result as the example above but more functional, we would do a recursion. I don't advise that every loop problem you solve with recursion.

1const logNumbers = (until, from = 0) => {
2 console.log(from)
3
4 if(from < until) {
5 logNumbers(until, from = from + 1)
6 }
7}
8
9logNumbers(10)
10//or
11logNumbers(10, 5)
12

This example is silly, and you would hopefully never write a recursive function for this to loop through a couple of numbers. However, there are some issues that recursion will most elegantly solve while keeping sanity. If you take an example of any tree traversal or manipulation, you will see that at many times' recursion will simplify your code way much and, in some cases, the only way to solve the problem. This means not necessarily improving the performance but improving readability.

Here is an example of a dumb binary search implementation. Again, it is not the best example to show where recursion will shine the most, but we see it is much cleaner than if we do it with loops.

1const binarySearch = (
2 numbers,
3 target,
4 low = 0,
5 high = numbers.length - 1
6) => {
7 const guess = Math.floor((high + low) / 2)
8
9 if (target == numbers[guess]) return guess
10
11 return target < numbers[guess] ?
12 binarySearch(numbers, target, low, guess - 1) :
13 binarySearch(numbers, target, guess + 1, high )
14}
15

One concern with this is the huge memory footprint. Making space on the stack for all function calls would be huge for some problems, but some optimizations, like tail call optimizations, can decrease the memory footprint.

Tail call optimization will allow you to call a function from another function without increasing the call stack. This optimization has been available to JS engines for some time

This is the first part of the series. To be notified when I publish the next part, subscribe to my newsletter. In the second part, I will continue exploring Lazy Evaluation and more.