JavaScript Functional programming demystified - Functions

ยท

6 min read

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 just start with the question, what is even a paradigm? To me the paradigm means the way of doing things, it defines how we think about problems and how we approach a problem.

Probably most natural for us is procedural, since it feels most familiar, as it is concrete and imperative, and we reason about programs like a recipe for a cake, we write concrete and super specific steps to make a cake. And functional programming is about... you would be surprised by making software with functions ๐Ÿ˜‚. Basically, writing software by composing pure functions, and describing functions in a declarative way, meaning that the program logic is expressed without explicitly describing the flow control.

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

Functions

We are all familiar with functions, right? But in a few words, we extract a procedure into a block of code and give it a name, so we can call it in different places and as much as we want with or without parameters. But we need to understand two things before moving further. First, JavaScript treats functions as first-class citizens. Functions are values like any other value, You can assign it, pass it to another function, or return it from a function. And that is one of the most important facts by which we can use FP principles. The second one is that we need to understand the concept of purity. Don't get me wrong of course it is hard to think about purity in an unpure world, but we are interested in pure enough.

Let's quickly fly over what would be the "pure" function, first, the function must be idempotent which means calling it once or many times it will always produce the same output, it must not interact with the outer world - global state, nor it should be effected in any way by the outer world i.e. not producing side effects, and It must not change the arguments passed in.

Side effects

A side effect is an event that is 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.

Lets take a look at some impure examples.

const number = 2 

const impure = () => 4 + number

second one

const impure = fruit => fruit.strain = true

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

const pureRandomNumber = () => 21

In the example, we can see that it passes all of our requirements to be pure, it's idempotent, and it will not interact nor change anything outside, heck it doesn't even have arguments.

const number = pureRandomNumber()

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

but since we live in the unpure world we can pass arguments to JavaScript functions even if we don't require them, and we can affect the outer world and crash the application, but we don't need to stress about these edge cases and consider these as pure functions.

The cool thing with pure functions is we can do a lot of optimizations since we are only dependent on inputs to generate outputs, right? For example, if we call the function twice with the same parameters, we can memorize the output and on the second call, we could, without extra computing, return the same result. This optimization is called memoization.

Loops

When shifting our thinking from procedural to functional programming, we notice that loops are truly imperative, we exactly tell the program how to increment numbers and what to do in the iteration.

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

The functional alternative would be recursion. So what is recursion? In a few words, recursion is a function that calls itself until certain criteria are met. It looks the same as loops to me. We run some code until some criteria are met, in the example above that would be the second item in the for declaration i <= 10;.

I absolutely love the quote from Graham Hutton

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

You just need a lot of practice to make it super easy to write them, and 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 to get the same result as in the example above but more functional, we would really on recursion. I don't advise that every loop problem you solve with recursion.

const logNumbers = (until, from = 0) => {    
    console.log(from)

    if(from < until) {
        logNumbers(until, from = from + 1)
    }
}

logNumbers(10)
//or
logNumbers(10, 5)

I know this example is silly, and you would hopefully never write a recursive function for this just to loop through couple numbers. But 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 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.


const binarySearch = (numbers, target, low = 0, high = numbers.length - 1) => {
    const guess = Math.floor((high + low) / 2)

    if (target == numbers[guess]) return guess

    return target < numbers[guess] ?
      binarySearch(numbers,  target, low, guess - 1) :
      binarySearch(numbers, target, guess + 1, high )
}

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 can be done to decrease memory footprint like tail call optimizations.

Tail call optimization will make it possible to call a function from another function without increasing the call stack. This optimization has been available to JS engines for some time, I think from es6 standard.

This is the first part of the series if you would like to be notified when I publish the next part ๐Ÿ‘‡ In the second part, I will be writing about Lazy Evaluation and more.

ย