Functional JavaScript: basic concepts

I am going to start a series of introductory articles about functional programming. We will address this subject as a kind of notes for newcomers, but also as notes for my future self. The goal won't be to cover exhaustively everything related to functional programming, but to provide some concepts and basic operations on which this paradigm is based.

Functional programming is built on some common operations, as composition and partial application, leading to some quite specialized techniques, as the transducers. Its goal is to minimize the shared states living within our applications as well as producing more declarative code —and therefore more legible— and, finally, allowing modules easier to test.

For this series we are going to use a common language: JavaScript. This is a language that works well with functional programming, although it was not purely conceinved for it as Erlang or Haskell. Because of this we will have to implement some structures and operations that other languages may provide natively,

Para esta serie vamos a utilizar un lenguaje común: Javascript. Este es un lenguaje que aunque funciona bien con programación funcional, no es un lenguaje pensado con este paradigma en mente como pueden serlo Erlang o Haskell. Because of this we will have to implement numerous structures that are natively found in other languages, which will teach us a lot about this type of programming.

As we will be covering functional programming in a concise way here, for those looking for a more exhaustive approach I recommend two resources:

In this first article we are going to discuss the most basic concepts and operations that we can find in functional programming. We will talk about:

  • Immutability
  • Purity
  • Arity
  • Currying
  • Partial application
  • Point-free style
  • Compose and pipe
  • Eager and lazy evaluation

We'll go over each of these concepts, presenting easy examples in JavaScript to keep things as straightforward as possible.

Immutability

One of the main challenges we may encounter in programming is shared state.

Let’s say we are working with JavaScript and have an object with a property a, stored in memory in the variable x with a certain value. If we then copy x into a new variable y what we are actually copying is the reference to x in y. From this point forward, any changes made to y will also affect x.

As you can imagine, this can be problematic, as it may lead to unexpected and imperceptible modifications to x.

language-javascript

const sherlock = { name: "Sherlock" };
⁠const bilbo = sherlock;
⁠bilbo.name = "Bilbo";
⁠⁠console.log(sherlock); // { name: "Bilbo" }
Run me in Playground 

To address this issue we may use the spread operator ... to extract the properties from john when creating a new object bob:

language-javascript

const sherlock = { name: "Sherlock" };
⁠const bilbo = { ...sherlock };
⁠bilbo.name = "Bilbo";
⁠sherlock.name = "Sherlock";
⁠console.log(sherlock); // { name: "Sherlock" }
Run me in Playground 

Keep in mind that the spread operator copies the first-level properties of an object by value, but nested properties are copied by reference with shallow copy. To avoid this issue, we can use the methods provided by the JSON object.

Another example of the problem with shared states could arise when dealing with concurrent operations. In this case, we have an increment operation whose result is stored in a sharedState object. But we also have a decrement operation that calculates a new value, which is also saved in sharedState. The result is a shared state: sharedState refers to both the value produced by increment and the one from decrement. Considering that both operations are impure, we end up with code that is hard to follow and difficult to test.

language-javascript

const sharedState = { type: "shared state", value: 0 };

⁠const increment = () => {
⁠ sharedState.value += 1;
⁠};

⁠const decrement = () => {
⁠ sharedState.value -= 1;
⁠};

⁠increment();
⁠decrement();
⁠decrement();
⁠increment();
⁠decrement();
⁠increment();

⁠⁠console.log(sharedState); // { type: "shared state", value: 0 }
Run me in Playground 

This example is not exactly concurrent, but it illustrates the challenge of tracking the value of our shared state throughout the application's flow. If we also had different modules consuming and modifying sharedState we might run into issues when trying to determine the state of our application at any given moment during its execution.

But all these issues can be easily solved by removing the shared state and generating a new state with each operation performed:

language-javascript

const initialState = { type: "non-shared state", value: 0 };

⁠const increment = (state) => ({
⁠ ...state,
⁠ value: state.value + 1,
⁠});

⁠const decrement = (state) => ({
⁠ ...state,
⁠ value: state.value - 1,
⁠});

⁠const stateIncremented = increment(initialState);
⁠const initialState2 = decrement(stateIncremented);
⁠const initialDecremented = decrement(initialState2);
⁠const initialState3 = increment(initialDecremented);
⁠const stateDecremented2 = decrement(initialState3);
⁠const initialState4 = increment(stateDecremented2);

⁠console.log(initialState4); // { type: "non-shared state", value: 0 }
Run me in Playground 

The variable names chosen aren't particularly elegant or expressive, but they were selected to highlight the advantage of this code over the previous one. We have moved the program's state from a mutable variable, whose content is only known at runtime, to an immutable variable, whose content is predictable at compile time. This makes the new version of our program easier to understand, debug, and test. In this sense immutability improves the predictability of our application.

We could even keep a history of all the states of our application, which would make it easier for us to debug and perform post-mortem analysis if any issues arise.

Purity

Functional programming aims for simplicity and predictability in our programs, which is why it relies heavily on functions. And for functions to be predictable they must return the same output for the same input. In this sense, a function represents an immutable relationship between two sets: input and output, and this relationship must remain constant.

language-javascript

⁠const pureFunction = (a, b) => a < b;
⁠console.log(pureFunction(1, 2)); // true

⁠const impureFunction = (a, b) => (Math.random() > 0.5 ? a < b : a > b);
⁠console.log(impureFunction(1, 2)); // true or false
Run me in Playground 

Purity makes predictability easier, but it also allows us to memoize function results, avoiding unnecessary computations.

Arity

    Arity refers to the number of parameters a function takes.

  • A function with an arity of 0 doesn’t take any parameters: it’s called nullary.
  • A function with an arity of 1 takes just one parameter: it’s unary.
  • A function with an arity of 2 takes two parameters: it’s binary.
  • A function with an arity of n can take any number of parameters: it’s called n-ary, also known as variadic.
language-javascript

⁠const nullaryFunction = () => "Hello world";
⁠console.log(nullaryFunction()); // hello world

⁠const unaryFunction = (greeting) => `${greeting} world`;
⁠console.log(unaryFunction("hello")); // hello world

⁠const binaryFunction = (greeting, subject) => `${greeting} ${subject}`;
⁠console.log(binaryFunction("hello", "world")); // hello world

⁠const nAryFunction = (...strings) =>
⁠ strings.reduce((acc, curr) => `${acc}${curr} `, "");
⁠console.log(nAryFunction("hello", "world", "and", "pluton"));
⁠// hello world and pluton
Arities, run me in Playground 

Nothing special here, just some terminology. But it's important, as unary functions are at the core of some operations in functional programming.

Currying

Currying is the process of transforming a function that takes multiple parameters —with an arity greater than 1— into a series of nested unary functions, that is, functions that take only one parameter.

language-javascript

⁠// Non curried function
⁠const sum = (a, b, c) => a + b + c;
⁠const result = sum(1, 2, 3);
⁠console.log(result); // 6

⁠// Curried function
⁠const sum = (a) => (b) => (c) => a + b + c;
⁠⁠const result = sum(1)(2)(3);
⁠console.log(result); // 6
Currying, run me in Playground 

As we'll see later, this is a key technique in functional programming: there are many operations where we need functions to have a similar structure —one input for one output— allowing them to be combined in different ways.

Partial application

Partial application refers to the process of transforming a function that takes various parameters into another function that only takes some of those parameters. It "fixes" certain arguments within its context and returns a new function that will take the remaining arguments, combining them with the prefixed ones.

This is really useful as it allows us to create functions with parameters that are pre-applied, which means we can specialize functions.

For example, let's say we have a function called createUrl that takes three string parameters: protocol, domain, and path, combining them and returning another string:

language-javascript

⁠const createUrl = (scheme, domain, path) => `${scheme}://${domain}/${path}`;

⁠const httpUrl = createUrl("http", "example.com", "hello");
⁠console.log(httpUrl); // "http://example.com/hello"

⁠const httpsUrl = createUrl("https", "example.com", "hello");
⁠console.log(httpsUrl); // "https://example.com/hello"
Run me in Playground 

We may probably use this function in multiple places in our code with "http" or "https". To write more declarative code and avoid passing the scheme parameter every time we need a URL we can create two specialized versions of createUrl: createHttpUrl and createHttpsUrl, applying the scheme parameter in the context of these functions.

language-javascript

⁠const createUrl = (scheme, domain, path) => `${scheme}://${domain}/${path}`;

⁠// Partial application: fixing `scheme` parameter
⁠const createUrlWithProtocol = (scheme) =>
⁠ (domain, path) => createUrl(scheme, domain, path);
⁠⁠
⁠⁠const createHttpUrl = createUrlWithProtocol("http");
⁠const createHttpsUrl = createUrlWithProtocol("https");

⁠const httpUrl = createHttpUrl("http", "example.com", "hello");
⁠console.log(httpUrl); // "http://example.com/hello"

⁠⁠const httpsUrl = createHttpsUrl("https", "example.com", "hello");
⁠console.log(httpsUrl); // "https://example.com/hello"
With partial application, run me in Playground 

These new functions are semantically more expressive, and they reduce the arity of the functions we use to create the URLs.

Point-free style

Point-free style is a way of writing functions where they are processed without referencing their parameters —points—.

Let's take a look at an example: when we pass a function as an argument to another function, it’s common practice to write it directly inside that function:

language-javascript

⁠const userName = users.map(
⁠ ({ first, lastName }) => `${first} ${lastName}`
⁠);
With parameters

In a point-free style we would extract the function declaration and store it in a variable, which is then passed to the new function.

language-javascript

⁠const composeName = ({ first, lastName }) => `${first} ${lastName}`;
⁠const userName = users.map(composeName⁠);
Point-free style

In general point-free style is more declarative and readable, and also makes function composition easier, as we’ll see later.

Compose and pipe

In algebra, function composition refers to the process of transforming a sequential set of unary functions into nested ones, so that the result of one function is passed as a parameter to the next, wrapping it.

Let's say we have two functions

f(x) = 2x\\ ⁠⁠g(x) = x^2

We express that we compose them with the following syntax:

f \circ g =(g(x))

Which means that we are executing g(x) first, and then f(x) with its result.

It’s important to emphasize that composition is not commutative:

f \circ g \neq g \circ f

although it is associative.:

f(g(h(x))) = (f \circ g)(h(x)) = f(g(h(x))).

This can easily be translated into JavaScript, as functions are first-class citizens in this language: we can treat them like variables, so any function can accept another as a parameter.

Given two functions, we can execute one inside the other:

language-javascript

⁠⁠const f = (x) => x * 2;
⁠const g = (x) => x * x;

⁠const result = f(g(3));
⁠console.log(result); // 18
Run me in Playground 

And let's say that this type of operation is something we do often: it would be interesting if we had a utility that, given two functions, would return them composed:

⁠language-javascript

⁠const f = (x) => x * 2;
⁠const g = (x) => x * x;

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

⁠const composed = compose(f, g);
⁠const resultComposed = composed(3); // 18
Run me in Playground 

In this case, we have a function that takes two parameters —arity 2—, performs partial application to fix them in the closure context, and returns a new function. This new function, when given a new parameter, will use the fixed ones to execute our nested functions, using the new parameter as the initial argument.

This way, the returned function is stored in the variable composed, which has f and g in its context, and will use them when called with a new argument (3).

Now let's say we want to compose three functions:

f \circ g \circ h = f(g(x))

Using the same logic, we can translate this into JavaScript with a function compose of arity 3:

language-javascript

⁠const f = (x) => x * 2;
⁠⁠const g = (x) => x * x;
⁠const h = (x) => x - 1;

⁠const compose = (f, g, h) => (x) => f(g(h(x)));

⁠const composed = compose(f, g, h);
⁠console.log(composed(3)); // 8
Run me in Playground 

It works perfectly, but it's not very scalable. The next step would be to create an n-ary function that, given any number of functions passed as parameters, returns them composed.

language-javascript

⁠const f = (x) => x * 2;
⁠⁠const g = (x) => x * x;
⁠const h = (x) => x - 1;

const compose = (...fns) => (x) =>
⁠ fns.reduceRight((acc, curr) => curr(acc), x);

⁠const composed = compose(f, g, h);
⁠console.log(composed(3)); // 8
Run me in Playground 

This function takes any number of functions as individual parameters. It distributes them into an array using the spread operator ..., and sets them within the closure context, returning a new function. This new function will be unary, receiving the initial data as a parameter to process our functions. It will then loop through the array of functions from right-to-left, executing them, which means it will execute the last function first with the initial data provided in the parameter.

What we've just done is create a compose function: a very handy n-ary utility for composing any number of functions as long as they are unary.

Now, let's say we want to compose our functions, but starting from the first one to the last: instead of using Array.reduceRight, we can use Array.reduce.

language-javascript

⁠const f = (x) => x * 2;
⁠⁠const g = (x) => x * x;
⁠const h = (x) => x - 1;

const pipe = (...fns) => (x) =>
fns.reduce((acc, curr) => curr(acc), x);

⁠const piped = pipe(f, g, h);
⁠console.log(piped(3)); // 35
Run me in Playground 

This type of utility is known as a pipe. As we mentioned earlier, composition is not commutative, so it returns a different result compared to the compose function.

Eager and lazy evaluation

By "eager evaluation," we refer to the computation model where a variable is evaluated immediately after the expression appears in the code.

For example, in JavaScript:

language-javascript

⁠⁠const sum = (a, b) => a + b;
const result = sum(1, 2); // Computed here
⁠console.log(result); // 3
Run me in Playground 

As we can see, the expression is evaluated when we call the function, not when the result of the function is consumed.

With lazy evaluation, on the other hand, computation is delayed until the result is actually used.

Except for the logical operators &&, || and ternary operator, JavaScript does not support lazy evaluation. This is a problem, since our code will always consume resources for the total number of operations reflected in the code.

However, lazy evaluation can be implemented in various ways. For example, using the mentioned logical operators —though in a very limited way— or by using generators:

language-javascript

⁠function* lazySum(a, b) {
⁠ yield a + b;
⁠}

⁠let result = lazySum(1, 3); // computation delayed
⁠console.log(result); // not evaluated yet
⁠console.log(result.next().value); // 4
Run me in Playground 

Here, the sum won't be executed when we call lazySum, but when we use the result and call next(). Later on, we'll explore operations in functional programming that are executed lazily.

Conclusion

That's all for today. All of these concepts and operations are quite common, but they are the foundation for the operations we'll cover soon. The code can be run in the TypeScript Playground, and is available at this repository.

If you have any question or if you found a mistake please ping me to the address at the footer.