Functional programming concepts explained in JavaScript

Thomas Rubattel
Valtech Switzerland
16 min readNov 5, 2018

--

Credits: unsplash.com

Last updated on 2024–02–17

“Any application that can be written in JavaScript, will eventually be written in JavaScript”. — Jeff Atwood, 2007

Functional programming got a lot of attention over the last years even though it’s there for a while, since McCarthy designed LISP in 1958. As an example, Java, the ultimate OOP language, supports functional programming features, like lambda expression and stream as of Java 8.

JavaScript by supporting function as first-class citizen embraces a taste of functional programming from the very beginning of its conception. Indeed Brendan Eich, its creator, was inspired by Scheme — which is a LISP dialect — when he designed it, back in May 1995. Function as first-class citizen means that functions can be assigned to variables, passed as argument to a function and returned from a function. This is so because functions are objects in JavaScript.

Even if JavaScript has typical imperative programming features such as reassignment, loop, function parameter passed by reference, JavaScript is keep evolving into the functional programming world by supporting new features such as the ones we gonna see, arrow function, rest operator, spread operator, higher order functions.

What is functional programming ?

Functional programming is a programming mindset which favors expressions over statements. The snippet below shows two functions, the first one written in an imperative style and another one written in a more expressive way.

const a = 10;
const b = 32;

const operation = (a, b, operator) => {
if(operator === '-'){
return a - b;
}
else if(operator === '*') {
return a * b;
}
else {
return a + b;
}
};

const operationExpressive = (a, b, operator) => operator(a, b);

console.log(operation(a, b, '+')); // 42
console.log(operationExpressive(a, b, (x, y) => x + y )); // 42

The expressive way uses the inversion of control mechanism — also called Hollywood Principle — making it more generic and dynamic — in the sense that it works at runtime too.

“Functional programming can be done in any programming language.”

Rúnar Bjarnason

Imperative programming paradigm

Imperative programming deals with states and modification of these states, called mutation. This paradigm is closely related to the Von Neumann architecture.

const word1 = "Here is";
const word2 = "a mutation";
let sentence = ""; // declaration and assignment

sentence = `${word1} ${word2}`; // mutation
console.log( sentence ); // Here is a mutation

Functional programming paradigm

The main goal of functional programming is to avoid mutation.

const word1 = "Here is";
const word2 = "no mutation";
const sentence = `${word1} ${word2}`; //declaration and assignment
console.log( sentence ); // Here is no mutation

For that sake, programs are made of mathematical functions. The counterpart of mathematical function in computer science is pure function.

Imperative programming vs functional programming

“Imperative programming is about thinking in terms of time whereas functional programming is about thinking in terms of space.”

Martin Odersky

Imperative programming depends on time since the order of mutations of shared data matters.

Functional programming encourages the creation of new data out of existing one. For that sake, functional programming languages have built-in data structure like linked list which can easily be expanded without changing the original data (persistent data structure).

Why the rise of functional programming ?

Since mid 2000s more and more programming languages supporting functional programming such as Scala, Elixir, Clojure, F#, Elm, PureScript, ReasonML, ClojureScript have emerged.

Likewise, popular JavaScript libraries such as Ramda, Lodash, Sanctuary, React, Redux, RxJS, Immutable.js, immer, support functional programming as well.

Why such a hype around functional programming ?

The main reason is that functional programming does not deal with state. So a program written in a functional manner does not depend on time. This remove complexity in software. It also makes programs more predictable, thus better testable and therefore less prone to bugs. Even more important, the code is easier to read.

Another argument in favour of functional programming is concurrency. Functional programming manages concurrency much better than imperative programming does, since again, functional programs are time independent.

Immutability

JavaScript does not support immutability by default, unlike Clojure, F#, Erlang, PureScript or Haskell do.

A misconception on that matter is the const syntax for declaring a variable. This syntax does not ensure data to be immutable for non-primitive data type. It just disallows to reassign a value to a variable.

const student = { name: 'Thomas' };
student.lastName = 'Rubattel';
console.log(student); // { name: 'Thomas', lastName: 'Rubattel' }

Do keep in mind that the spread operator only does a shallow copy and not a complete copy, called deep copy, of non-primitive data type.

const student = { name: 'Thomas', grades: [5, 4.5] };
const studentClone = {...student};
console.log( student.grades === studentClone.grades ); // true

Same thing about destructuring.

const student = { name: 'Thomas', grades: [5, 4.5] };
const {name, grades} = student;
console.log( student.grades === grades ); // true

To sum up const, spread and destructuring do not prevent data to be changed. The only way to make sure variables will not be mutated is by using Object.freeze().

Pure function

A function is pure if both of the following conditions are fulfilled :

  • always returns the same output given the same input (deterministic)
  • does not change states outside of its scope (no side-effect)
const numbers = [10, 3];

const appendImpure = (item, array) => {
const length = array.length;
array[length] = item;
return array;
};

const appendPure = (item, array) => [...array, item];

console.log("impure", appendImpure(20, numbers)); // [10, 3, 20]
console.log("numbers", numbers); // [10, 3, 20]

console.log("pure", appendPure(32, numbers)); // [10, 3, 20, 32]
console.log("numbers", numbers); // [10, 3, 20]

The function appendImpure() is not a pure function since by calling it, the variable numbers get modified. This is due to the fact that in JavaScript objects as parameter are passed by reference and, as we saw before, theses are mutable.

A function doing one of the following things is impure : DOM access, HTTP request, local storage access, file access, database query, getting user input, returning a random number, returning the current time, console.log.

There are a lot of debates and different views on purity. We share Kyle Simpson’s opinion that a function depending on a variable outside of its scope is still a pure function if that variable is immutable. Reginald Braithwaite has another approach on that. This way, the following function circleArea is pure — it would make the function more portable by passing the PI constant as an argument to circleArea but that’s for the sake of the example.

const PI = 3.14;

const circleArea = (radius) => PI * radius * radius;

Anonymous function

As its name suggests an anonymous function is a function having no name. In some programming languages, anonymous functions are called lambda, e.g. in Scheme.

// named function
function namedSum( numbers ){
return numbers.reduce( (acc, number) => acc + number, 0);
}

// anonymous function assigned to a variable
const sum = (numbers) => numbers.reduce((acc, number) => acc + number, 0);
console.log( sum([1, 2, 5]) ); // 8

Higher order functions

Higher order functions are functions taking a function as parameter or returning a function.

.map(), .filter(), .find(), .some(), .reduce() (see code snippet above) as well as .then(), setTimeout(), are higher order functions.

Callback

A callback is a function passed as a parameter to another function.

One very common usage of callback is the continuation-passing-style (CPS), for having things done asynchronously. In such a case, the callback will be called by the higher order function upon event occurrence (e.g. HTTP request has been resolved). The parameter of the callback is the result that the higher order function is waiting for.

Recursion

Pure functional programming languages like Haskell do not have built-in loop — since a loop does mutate a variable — but use recursion instead.

The rest operator helps in doing recursion with arrays.

The code snippet below splits up the first element of the array and the remaining elements, means the rest. Then the first element is summed up with the rest by redoing the same process with the rest and so on.

const sum = (numbers) => {
const [first, ...rest] = numbers;
return numbers.length === 0 ? 0 : first + sum(rest);
};

console.log(sum([67, 3])); // 70

Let’s simulate the recursive calls with the following call sum([67, 3]) :

67 + sum([3])
3 + sum([])
0

Once the so-called base case (also called halting case or exit case or edge condition or terminating condition) has been reached, the computation gets back all the way up to the initial call and thus can terminate, so 67 + 3 + 0.

rest also called tail is an array or linked list — Credits: http://learnyouahaskell.com/

Tail call optimization

When the recursive calls exceed the available memory, called the call stack (or just stack) a stack overflow might occur, hence the name. For avoiding this issue, JavaScript (since ES2015) specifies an optimization known under tail call optimization. The goal is to reuse the stack frame of the main function.

On that purpose we need to rewrite the function for avoiding recursive stack frames. In the previous code snippet this is due to first + sum(rest). We can rewrite it by using an accumulator. This new version is known under tail-recursive.

const sum = (numbers) => {
const sumAux = (numbers, acc) => {
const [first, ...rest] = numbers;
return numbers.length === 0 ? acc : sumAux(rest, first+acc);
};

return sumAux(numbers, 0);
};

console.log(sum([10, 102, 0, 3])); // 115

Tail call recursion was introduced by Sussman and Steele, the designers of Scheme. They suggested the continuation-passing-style for optimizing recursive functions. Scheme, which was an attempt to implement the actor model, had a profound influence on functional programming.

Closure

A closure is a function accessing a variable part of the scope of a hosting function (so-called enclosing scope).

In the following code snippet, we’d like to get only the grades which are above or equal to a certain threshold. Let’s define a function taking the threshold as parameter and returning a function, — the so-called closure (grade) => grade >= threshold, which has access to the scope of the main one.

const grades = [3.5, 3, 2, 6, 4];

const filterGrades = (threshold) => (grade) => grade >= threshold;

console.log(grades.filter( filterGrades(4) )); // [6, 4]

In respect to the memory, variables accessed by the closure in the enclosing scope (so-called free variables) persist even after the outer function call (not garbage collected yet). To relate to the code snippet above, the callback of the .filter function still have access to the value of threshold4—, despite the fact that filterGrades has been already called and therefore is no longer in the stack.

Variables that a closure accesses in the enclosing scope are stored in the heap.

With respect of the purity, the access of variables of the enclosing scope (free variables) by the closure (threshold in the example above) does not affect the purity of the closure if these variables are immutable.

Scope of the inner function encloses — hence the name — scope of the outer function

Data encapsulation using the module pattern is a use case of closure. React Hooks use closure under the hood.

Point-free style

This technique also known as tacit programming means that the function’s arguments are implicit.

Let’s have an example where point-free style shines. In the snippet below, the two combinators, tap and compose, are composed with each other. The point-free version is way less verbose.

const tap = (f) => args => {
f(args);
return args;
}

const compose = f => g => x => f(g(x)) ;

const double = x => 2 * x;

const square = x => x * x;

// Non point-free style
const logNonPF = (...args) => console.log(...args);
const resultNonPF =
tap(logNonPF)(((a) => compose((b) => double(b))((c)=> square(c))(a))(3));


// point-free style
const logPF = console.log;
const resultPF = tap(logPF)(compose(double)(square)(3));

Point-free style originates from the combinatory logic developed by Moses Schönfinkel and Haskell Curry. This mathematical theory varies a bit from the lambda calculus in the sense that functions do not have free variables.

Function composition

Function composition, which has its roots in mathematics, consists in defining a new function out of others, which are applied one after another.

const compose = (student) => hasPassed(getAverage(student));

Or in a slightly more readable and in an automated way :

const compose = (...fs) => (x) => fs.reduceRight((acc, f) => f(acc), x);

const composition = compose(
hasPassed,
getAverage
);

In a full example :

const getAverage = (student) => {
const gradesNb = student.grades.length;
return gradesNb === 0 ? 0 : student.grades.reduce(
(acc, grade) => acc + grade, 0) / gradesNb;
};

const hasPassed = (average) => average >= 4;

const compose = (...fs) => (x) => fs.reduceRight((acc, f) => f(acc), x);

const student = { name: 'Thomas', grades: [3, 3] };
console.log(compose(hasPassed, getAverage)(student)); // false

flow in Lodash, pipe in Rambda implements the compose function that we created above.

The equivalent code in an imperative approach should be avoided.

// Don't do that 👎🏻
const hasPassedImperative = (student) => {
const average = getAverage(student);
const studentHasPassed = hasPassed(average);
return studentHasPassed;
}

Currying

Currying is a mathematical insight named after the mathematician Haskell Curry. In the lambda calculus, lambda expressions have a single argument. To overcome this, Haskell used the currying technique.

Currying is about rewriting a function taking n parameters into a sequence of n functions taking one single parameter each.

Even though currying is not natively supported in JavaScript, unlike in Haskell or Scala, there are ways to implement it, for example by using closures, like in the example below.

Currying and closure are closed concepts, but originated from different field. Currying is a mathematical concept. Closure is a computer science concept related to scope and a way to implement currying.

Use cases of currying are function composition — especially automated composition — and code reusability (function factory), as the example below shows. The snippet below illustrates how functions can be reused by doing a partial application. Topics below, function decorator and thunk, are other use cases of currying.

const a = 10;
const b = 32;

// WITHOUT CURRYING
const operation = (a, b, operator) => operator(a, b);
console.log(operation(a, b, (a, b) => a + b)); // 42

// WITH CURRYING
const operation = (operator) => (a) => (b) => operator(a, b);

const add = operation((a, b) => a + b); // partial application
console.log(add(a)(b)); // 42

Collection function composition

Chaining higher order functions such as for example .filter() after having done a .map() is nothing but function composition.

const getAverage = (grades) => 
grades.length === 0 ? 0 :
grades.reduce((acc, grade) => acc + grade, 0) / grades.length;

const withAverage = (student) =>
({...student, avg: getAverage(student.grades)});

const hasPassed = (student) => student.avg >= 4;

const students = [
{ name: 'Thomas', grades: [3, 3] },
{ name: 'Lia', grades: [4, 3, 5] }
];

const passed = students
.map(withAverage)
.filter(hasPassed);

console.log(passed); // [{name: 'Lia', grades: [4,3,5], avg: 4}]

Reducers

So far we got the point of function composition. The thing is that chaining .map() and .filter() as we did, from a performance perspective, is not great, since we iterate multiple times over the array.

Let’s generalize slightly our code and write two helpers genMapReducer() and genFilterReducer() which both return a function called a reducer. A reducer (also called reducing function) is a function : (acc, item) => acc.

const genMapReducer = (mapper) => (acc, item) => acc.concat(mapper(item));

const genFilterReducer = (predicate) => (acc, item) =>
predicate(item) ? acc.concat(item) : acc;

const getAverage = (grades) =>
grades.length === 0 ? 0 :
grades.reduce((acc, grade) => acc + grade, 0) / grades.length;

const withAverage = (student) =>
({...student, avg: getAverage(student.grades)});

const hasPassed = (student) => student.avg >= 4;

const mapWithAverage = genMapReducer(withAverage);
const filterHasPassed = genFilterReducer(hasPassed);

const students = [
{name: 'Thomas', grades: [3, 3]},
{name: 'Lia', grades: [4, 3, 5]}
];

const passed = students
.reduce(mapWithAverage, [])
.reduce(filterHasPassed, []);

console.log(passed); // [{name: 'Lia', grades: [4,3,5], avg: 4}]

.reduce() generalizes .map() and .filter(). So we can pass any kind of process (filtering, transformation, etc.) to .reduce() by passing a parameterized reducer to it. But we still iterate multiple times over the collection.

Transducers

A transducer is a concept conceived in 2014 by Rich Hickey, the conceiver of Clojure, and ported to JavaScript by some libraries such as Ramda. A transducer is an abstraction that allows to compose reducers. Thus we can iterate over the collection at once after having composed reducers.

const genMapTransducer = (mapper) => (concatenator) =>
(acc, item) => concatenator(acc, mapper(item));

const genFilterTransducer = (predicate) => (concatenator) =>
(acc, item) => predicate(item) ? concatenator(acc, item) : acc;

const getAverage = (grades) =>
grades.length === 0 ? 0 :
grades.reduce((acc, grade) => acc + grade, 0) / grades.length;

const withAverage = (student) =>
({...student, avg: getAverage(student.grades)});

const hasPassed = (student) => student.avg >= 4;

const mapWithAverage = genMapTransducer(withAverage);
const filterHasPassed = genFilterTransducer(hasPassed);
const concatenator = (acc, item) => acc.concat(item);

const students = [
{name: 'Thomas', grades: [3, 3]},
{name: 'Lia', grades: [4, 3, 5]}
];

const passed = students
.reduce(mapWithAverage(filterHasPassed(concatenator)), []);

console.log(passed); // [{name: 'Lia', grades: [4,3,5], avg: 4}]

What we have done new here is to extract the concatenator, namely in our example acc.concat() and parameterized it with a closure in the two helpers, genMapTransducer() and genFilterTransducer(). These helpers now both generate a transducer. This parameterization allows to delay the concatenation operation.

Simply put a transducer is a function that makes possible to compose reducers. Technically, it’s a function taking a reducer and returning a reducer:

((acc, item) => acc) => (acc, item) => acc

As an aside, we can also compose in an automated way as we did above :

const compose = (...fs) => (x) => fs.reduceRight((acc, f) => f(acc), x);

const composition = compose(
mapWithAverage,
filterHasPassed
);

const xform = composition(concatenator);
const passed = students.reduce(xform, []);

Function decorator

A function decorator adds some features to an existing function that you may not want to change or that you may not be able to change (e.g. third party library or function passed as props to a React component).

Technically, a function decorator is a function taking a function to be decorated as parameter, by using the dependency injection technique, and returning a new function. The generic pattern of the function decorator is the following.

const decorator = (f) => (...args) => {
// do something

f(...args); // no recursion here, just call the callback
// do something
};

This technique is mostly used when the function decorator and the function to be decorated are not composable, for example when one of these does not return anything.

The original function’s parameters and the original function itself are decoupled by using the currying technique, which allows the new function to have the same API as the original one.

In React, a higher-order component (HOC) of a functional component is a function decorator. Other examples of function decorator are function memoization (see below) or function debouncing.

Redux supports a middleware architecture just as Express.js. A middleware is a layer providing a service. A layer is a function decorating the next layer and so on recursively.

Memoization

The term memoization was introduced by Donald Michie in a paper in 1968. Memoization is a technique for keeping the result of a computation. In case the same computation is needed later on, the computation does not need to be redone. The result is stored in memory and just needs to be retrieved. We are going to discuss here the memoization of functions. Pure functions are way easier to memoize.

Let’s create a function decorator called memoize, which takes a function doing any kind of computation and returning a closure whose arguments are the arguments of the first one. The closure has access to the cache map acting as a cache.

const memoize = (f) => {
const cache = new Map();
return (arg) => {
if (cache.get(arg)) {
console.log("Cached : ", cache.get(arg));
return cache.get(arg);
}
else {
const result = f(arg);
cache.set(arg, result);
console.log("Computed : ", result);
return result;
}
};
};

const getAverage = (grades) =>
grades.length === 0 ? 0 :
grades.reduce((acc, number) => acc + number, 0) / grades.length;

const getAverageMemoized = memoize(getAverage);

const grades = [4, 6];

getAverageMemoized(grades); // Computed : 5
getAverageMemoized(grades); // Cached : 5

Thus, the decorated function — getAverageMemoized— prevents a computation to be redone — in this case the average of numbers.

React.memo(), introduced in React 16.6.0, works that way.

Laziness

Laziness is about doing computation only on demand.

Call-by-value vs call-by-name are strategies at the compiler/interpreter level referring to the order in which expressions are evaluated. JavaScript has a call-by-value strategy unlike Haskell. Thus, JavaScript does not support laziness natively, making it an eager evaluated language.

As an example, hasPassed(getAverage(student)) in JavaScript would first evaluate the parameter and call the outer function like that hasPassed(4.5). In a lazy evaluated mode the parameters are evaluated when they are actually used in the body of the outer function and not before.

ES2015 brought a new feature, coming from Python, called generator, allowing to do laziness while processing a finite or infinite data flow (called stream in Scheme and called observable in RxJS). The snippet below is written with RxJS. The whole collection will not be processed. The flow is processed element by element.

import { from } from "rxjs";
import { map, filter, take } from "rxjs/operators";

const getAverage = grades => {
const gradesNb = grades.length;
return gradesNb === 0 ? 0
: grades.reduce((acc, grade) => acc + grade, 0) / gradesNb;
};

const hasPassed = (student) => student.avg >= 4;

const withAverage = (student) => ({
...student,
avg: getAverage(student.grades)
});

const students = [
{ name: "Hiro", grades: [4, 2] },
{ name: "Ryu", grades: [3, 3.5] },
{ name: "Rie", grades: [6, 3] },
{ name: "Asako", grades: [6, 5] },
{ name: "Masaru", grades: [4, 3] },
{ name: "Sakon", grades: [3, 3, 4] }
];

const hasPassed = from(students)
.pipe(
map(withAverage),
filter(hasPassed),
take(1)
)
.subscribe(x => console.log(x));
// { name: 'Rie', grades: [ 6, 3 ], avg: 4.5 }

Delayed evaluation is another technique part of the laziness topic. This can be done by wrapping an expression in a function. The delayed expression is called in this case a thunk. This technique is for example used in Redux-thunk for dealing with asynchronicity and for having side-effects at a single point of the application. Indeed, the function generating the thunk, namely getPostDelayed, which will be called inside a React component, is pure. The thunk, which has a side-effect, is called by the middleware.

const getPost = (id) => fetch(`domain.com/post/${id}`);

const getPostDelayed = (id) => () => fetch(`domain.com/post/${id}`);

const thunk = getPostDelayed(56);

Wrap up

  1. Do write pure function.
  2. Try to avoid mutations. If you still have to do so, do isolate mutations in function scope (like we did in memoization) so that function’s purity won’t be affected.
  3. Do not use the this context. this is not immutable and thus can lead to unpredictable behaviour.
  4. Quit chaining higher order functions on collections (use transducer, e.g. with Ramda or observable, e.g. with RxJS).
  5. Prefer composition over OOP.
  6. Isolate the side-effects (HTTP request, local storage access, logging, etc.) at the boundary of your application (middleware).
  7. Closure and currying are related concepts. Closure is a way to implement currying which is a mathematical concept.

--

--