Functional Programming Fundamentals
JavaScript has been a multi-paradigm programming language since its inception back in 1995. It allows us to take advantage of an object-oriented programming (OOP) style along with a functional programming style. The same can be said of TypeScript. However, for functional programming, TypeScript is even better suited than JavaScript because, as we will learn in this chapter, static type systems and type inference are both very important features in functional programming languages such as the ML family of programming languages, for example.
The JavaScript and TypeScript ecosystems have experienced a significant increase in interest in functional programming over the last few years. I believe that this increase in interest can be attributed to the success of React. React is a library developed by Facebook for building user interfaces, and it is highly influenced by some core functional programming concepts.
In this chapter, we will focus on learning some of the most basic functional programming concepts and principles.
In this chapter, you will learn about the following:
- The main characteristics of functional programming
- The main benefits of functional programming
- Pure functions
- side-effects
- Immutability
- Function arity
- Higher-order functions
- Laziness
Is TypeScript a functional programming language?
The answer to this question is yes, but only in part. TypeScript is a multi-paradigm programming language and, as a result, it includes many influences from both OOP languages and functional programming paradigms.
However, if we focus on TypeScript as a functional programming language, we can observe that it is not a purely functional programming language because, for example, the TypeScript compiler doesn't force our code to be free of side-effects.
Not being a purely functional programming language should not be interpreted as something negative. TypeScript provides us with an extensive set of features that allow us to take advantage of some of the best features of the world of OOP languages and the world of functional programming languages. This has allowed TypeScript-type systems to attain a very good compromise between productivity and formality.
The benefits of functional programming
Writing TypeScript code using a functional programming style has many benefits, among which we can highlight the following:
- Our code is testable: If we try to write our functions as pure functions, we will be able to write unit tests extremely easily. We will learn more about pure functions later in this chapter.
- Our code is easy to reason about: Functional programming can seem hard to understand for developers with a lack of experience in functional programming. However, when an application is implemented correctly using the functional programming paradigm, the results are very small functions (often one-line functions) and very declarative APIs that can be reasoned about with ease. Also, pure functions only work with their arguments, which means that when we want to understand what a function does, we only need to examine the function itself and we don't need to be concerned about any other external variables.
- Concurrency: Most of our functions are stateless, and our code is mostly stateless. We push state out of the core of our application, which makes our applications much more likely to be able to support many concurrent operations and it will be more scalable. We will learn more about stateless code later in this chapter.
- Simpler caching: Caching strategies to cache results become much simpler when we can predict the output of a function given its arguments.
Introducing functional programming
Functional programming (FP) is a programming paradigm that receives its name from the way we build applications when we use it. In a programming paradigm such as OOP, the main building blocks that we use to create an application are objects (objects are declared using classes). However, in FP, we use functions as the main building block in our applications.
Each new programming paradigm introduces a series of concepts and ideas associated with it. Some of these concepts are universal and are also of interest while learning a different programming paradigm. In OOP, we have concepts such as inheritance, encapsulation, and polymorphism. In functional programming, concepts include higher-order functions, function partial application, immutability, and referential transparency. We are going to examine some of these concepts in this chapter.
Michael Feathers, the author of the SOLID acronym and many other well-known software engineering principles, once wrote the following:
– Michael Feathers
The preceding quote mentions moving parts. We should understand these moving parts as state changes (also known as state mutations). In OOP, we use encapsulation to prevent objects from being aware of the state mutations of other objects. In functional programming, we try to avoid dealing with state mutations instead of encapsulating them.
FP reduces the number of places in which state changes take place within an application and tries to move these places into the boundaries of the application to try to keep the application's core stateless.
A mutable state is bad because it makes the behavior of our code harder to predict. Take the following function, for example:
function isIndexPage() {
return window.location.pathname === "/";
}
The preceding code snippet declared a function named isIndexPage. This function can be used to check whether the current page is the root page in a web application based on the current path.
The path is some data that changes all the time, so we can consider it a piece of state. If we try to predict the result of invoking the isIndexPage, we will need to know the current state. The problem is that we could wrongly assume that the state has not changed since the last known state. We can solve this problem by transforming the preceding function into what is known in FP as a pure function, as we will learn in the following section.
Pure functions
FP introduces a number of concepts and principles that will help us to improve the predictability of our code. In this section, we are going to learn about one of these core concepts—pure functions.
A function can be considered pure when it returns a value that is computed using only the arguments passed to it. Also, a pure function avoids mutating its arguments or any other external variables. As a result, a pure function always returns the same value given the same arguments, independently of when it is invoked.
The isIndexPage function declared in the preceding section is not a pure function because it accesses the pathname variable, which has not been passed as an argument to the function. We can transform the preceding function into a pure function by rewriting it as follows:
function isIndexPage(pathname: string) {
return pathname === "/";
}
Even though this is a basic example, we can easily perceive that the newer version is much easier to predict. Pure functions help us to make our code easier to understand, maintain, and test.
Imagine that we wanted to write a unit test for the impure version of the isIndexPage function. We would encounter some problems when trying to write a test because the function uses the window.location object. We could overcome this issue by using a mocking framework, but it would add a lot of complexity to our unit tests just because we didn't use a pure function.
On the other hand, testing the pure version of the isIndexPage function would be straightforward, as follows:
function shouldReturnTrueWhenPathIsIndex(){
let expected = true;
let result = isIndexPage("/");
if (expected !== result) {
throw new Error('Expected ${expected} to equals ${result}');
}
}
function shouldReturnFalseWhenPathIsNotIndex() {
let expected = false;
let result = isIndexPage("/someotherpage");
if (expected !== result) {
throw new Error('Expected ${expected} to equals ${result}');
}
}
Now that we understand how functional programming helps us to write better code by avoiding state mutations, we can learn about side-effects and referential transparency.
side-effects
In the preceding section, we learned that a pure function returns a value that can be computed using only the arguments passed to it. A pure function also avoids mutating its arguments or any other external variable that is not passed to the function as an argument. In FP terminology, it is common to say that a pure function is a function that has no side-effects, which means that, when we invoke a pure function, we can expect that the function is not going to interfere (through a state mutation) with any other component in our application.
Certain programming languages, such as Haskell, can ensure that an application is free of side-effects using their type system. TypeScript has fantastic interoperability with JavaScript, but the downside of this, compared to a more isolated language such as Haskell, is that the type system is not able to guarantee that our application is free from side-effects. However, we can use some FP techniques to improve the type safety of our TypeScript applications. Let's take a look at an example:
interface User {
ageInMonths: number;
name: string;
}
function findUserAgeByName(users: User[], name: string): number {
if (users.length == 0) {
throw new Error("There are no users!");
}
const user = users.find(u => u.name === name);
if (!user) {
throw new Error("User not found!");
} else {
return user.ageInMonths;
}
}
The preceding function returns a number. The code compiles without issues. The problem is that the function does not always return a number. As a result, we can consume the function as follows and our code will compile and throw an exception at runtime:
const users = [
{ ageInMonths: 1, name: "Remo" },
{ ageInMonths: 2, name: "Leo" }
];
// The variable userAge1 is as number
const userAge1 = findUserAgeByName(users, "Remo");
console.log('Remo is ${userAge1 / 12} years old!');
// The variable userAge2 is a number but the function throws!
const userAge2 = findUserAgeByName([], "Leo"); // Error
console.log('Leo is ${userAge2 / 12} years old!');
The following example showcases a new implementation of the preceding function. This time, instead of returning a number, we will explicitly return a promise. The promise forces us to then use the handler. This handler is only executed if the promise is fulfilled, which means that if the function returns an error, we will never try to convert the age to years:
function safeFindUserAgeByName(users: User[], name: string): Promise<number> {
if (users.length == 0) {
return Promise.reject(new Error("There are no users!"));
}
const user = users.find(u => u.name === name);
if (!user) {
return Promise.reject(new Error("User not found!"));
} else {
return Promise.resolve(user.ageInMonths);
}
}
safeFindUserAgeByName(users, "Remo")
.then(userAge1 => console.log('Remo is ${userAge1 / 12} years old!'));
safeFindUserAgeByName([], "Leo") // Error
.then(userAge1 => console.log('Leo is ${userAge1 / 12} years old!'));
The Promise type helps us to prevent errors because it expresses potential errors in an explicit way. In programming languages such as Haskell, this is the default behavior of the type system, but, in programming languages such as TypeScript, it is up to us to use types in a safer way.
Referential transparency
Referential transparency is another concept closely related to pure functions and side-effects. A function is pure when it is free from side-effects. An expression is said to be referentially transparent when it can be replaced with its corresponding value without changing the application's behavior. For example, if we are using the following in our code:
let result = isIndexPage("/");
We know that the isIndexPage function is referentially transparent because it would be safe to substitute it for its return type. In this case, we know that when we invoke the isIndexPage function with / as an argument, the function will always return true, which means that it would be safe to do the following:
let result = true;
A pure function is a referentially transparent expression. An expression that is not referentially transparent is known as referentially opaque.
Stateless versus stateful
Pure functions and referentially transparent expressions are stateless. A piece of code is stateless when its outcomes are not influenced by previous events. For example, the results of the isIndexPage function will not be influenced by the number of times that we invoke it, or by the moment in time when we invoke it.
The opposite of stateless code is stateful code. Stateless code is very difficult to test and becomes a problem when we are trying to implement scalable and resilient systems. Resilient systems are systems that can handle server failures; there is usually more than one instance of a service, and if one of them crashes, others can continue handling traffic. Also, new instances are created automatically after one of the instances has crashed. This becomes very difficult if our servers are stateful because we need to save the current state before a crash and restore the state before we spin up a new instance. The whole process becomes much simpler when we design our servers to be stateless.
With the arrival of the cloud computing revolution, these kinds of system have become more common, and this has led to an interest in functional programming languages and design principles because functional programming encourages us to write stateless code. The opposite can be said of OOP because classes are the main construct in OOP applications. Classes encapsulate state properties that are then modified by methods, which encourages methods to be stateful and not pure.
Declarative versus imperative programming
The advocates of the FP paradigm often use declarative programming as one of its main benefits. Declarative programming is not necessarily exclusive to functional programming, but FP certainly encourages or facilitates this programming style. Before we take a look at some examples, we are going to define declarative programming and imperative programming:
- Imperative programming is a programming paradigm that uses statements that change a program's state. In much the same way that the imperative mood in natural languages expresses commands, an imperative program consists of commands for the computer to perform. Imperative programming focuses on describing how a program operates.
- Declarative programming is a programming paradigm that expresses the logic of a computation without describing its control flow. Many languages that apply this style attempt to minimize or eliminate side-effects by describing what the program must accomplish in terms of the problem domain, rather than describing how to accomplish it as a sequence of steps.
The following example calculates the average result of an exam given a collection of objects that contains an ID and a result for a list of students. This example uses an imperative programming style because, as we can see, it uses control flow statements (for). The example is also clearly imperative because it mutates a state. The total variable is declared using the let keyword because it is mutated as many times as results are contained in the results array:
interface Result {
id: number;
result:number;
}
const results: Result[] = [
{ id: 1, result: 64 },
{ id: 2, result: 87 },
{ id: 3, result: 89 }
];
function avg(arr: Result[]) {
let total = 0;
for (var i = 0; i < arr.length; i++) {
total += arr[i].result;
}
return total / arr.length;
}
const resultsAvg = avg(results);
console.log(resultsAvg);
On the other hand, the following example is declarative because there are no control flow statements and there are no state mutations:
interface Result {
id: number;
result:number;
}
const results: Result[] = [
{ id: 1, result: 64 },
{ id: 2, result: 87 },
{ id: 3, result: 89 }
];
const add = (a: number, b: number) => a + b;
const division = (a: number, b: number) => a / b;
const avg = (arr: Result[]) =>
division(arr.map(a => a.result).reduce(add, 0), arr.length)
const resultsAvg = avg(results);
console.log(resultsAvg);
While the previous example is declarative, it is not as declarative as it could be. The following example takes the declarative style one step further so we can get an idea of how a piece of declarative code may appear. Don't worry if you don't understand everything in this example right now. We will be able to understand it once we learn more about functional programming techniques later in this book. Note how the program is now defined as a set of very small functions that don't mutate the state and that also don't use control flow statements. These functions are reusable because they are independent of the problem that we are trying to solve. For example, the avg function can calculate an average, but it doesn't need to be an average of results:
const add = (a: number, b: number) => a + b;
const addMany = (...args: number[]) => args.reduce(add, 0);
const div = (a: number, b: number) => a / b;
const mapProp = <T>(k: keyof T, arr: T[]) => arr.map(a => a[k]);
const avg = (arr: number[]) => div(addMany(...arr), arr.length);
interface Result {
id: number;
result:number;
}
const results: Result[] = [
{ id: 1, result: 64 },
{ id: 2, result: 87 },
{ id: 3, result: 89 }
];
const resultsAvg = avg(mapProp("result", results));
console.log(resultsAvg);
The actual code that is specific to the problem that we are trying to solve is very small:
const resultsAvg = avg(mapProp("result", results));
This code is not reusable, but the add, addMany, div, mapProp, and avg functions are reusable. This demonstrates how declarative programming can lead to more reusable code than imperative programming.
Immutability
Immutability refers to the inability to change the value of a variable after a value has been assigned to it. Purely functional programming languages include immutable implementations of common data structures. For example, when we add an element to an array, we are mutating the original array. However, if we use an immutable array and we try to add a new element to it, the original array will not be mutated, and we will add the new item to a copy of it.
The following code snippet declares a class named ImmutableList that demonstrates how it is possible to implement an immutable array:
class ImmutableList<T> {
private readonly _list: ReadonlyArray<T>;
private _deepCloneItem(item: T) {
return JSON.parse(JSON.stringify(item)) as T;
}
public constructor(initialValue?: Array<T>) {
this._list = initialValue || [];
}
public add(newItem: T) {
const clone = this._list.map(i => this._deepCloneItem(i));
const newList = [...clone, newItem];
const newInstance = new ImmutableList<T>(newList);
return newInstance;
}
public remove(
item: T,
areEqual: (a: T, b: T) => boolean = (a, b) => a === b
) {
const newList = this._list.filter(i => !areEqual(item, i))
.map(i => this._deepCloneItem(i));
const newInstance = new ImmutableList<T>(newList);
return newInstance;
}
public get(index: number): T | undefined {
const item = this._list[index];
return item ? this._deepCloneItem(item) : undefined;
}
public find(filter: (item: T) => boolean) {
const item = this._list.find(filter);
return item ? this._deepCloneItem(item) : undefined;
}
}
Every time we add an item to, or remove it from, the immutable array, we create a new instance of the immutable array. This implementation is very inefficient, but it demonstrates the basic idea. We are going to create a quick test to demonstrate how the preceding class works. We are going to use some data regarding superheroes:
interface Hero {
name: string;
powers: string[];
}
const heroes = [
{
name: "Spiderman",
powers: [
"wall-crawling",
"enhanced strength",
"enhanced speed",
"spider-Sense"
]
},
{
name: "Superman",
powers: [
"flight",
"superhuman strength",
"x-ray vision",
"super-speed"
]
}
];
const hulk = {
name: "Hulk",
powers: [
"superhuman strength",
"superhuman speed",
"superhuman Stamina",
"superhuman durability"
]
};
We can now use the preceding data to create a new immutable list instance. When we add a new superhero to the list, a new immutable list is created. If we try to search for the superhero Hulk in the two immutable lists, we will observe that only the second list contains it. We can also compare both lists to observe that they are two different objects, demonstrated as follows:
const myList = new ImmutableList<Hero>(heroes);
const myList2 = myList.add(hulk);
const result1 = myList.find((h => h.name === "Hulk"));
const result2 = myList2.find((h => h.name === "Hulk"));
const areEqual = myList2 === myList;
console.log(result1); // undefined
console.log(result2); // { name: "Hulk", powers: Array(4) }
console.log(areEqual); // false
Creating our own immutable data structures is, in most cases, not necessary. In a real-world application, we can use libraries such as Immutable.js to enjoy immutable data structures.
Functions as first-class citizens
It is common to find mentions of functions as first-class citizens in the FP literature. We say that a function is a first-class citizen when it can do everything that a variable can do, which means that functions can be passed to other functions as an argument. For example, the following function takes a function as its second argument:
function find<T>(arr: T[], filter: (i: T) => boolean) {
return arr.filter(filter);
}
find(heroes, (h) => h.name === "Spiderman");
Or, it is returned by another function. For example, the following function takes a function as its only argument and returns a function:
function find<T>(filter: (i: T) => boolean) {
return (arr: T[]) => {
return arr.filter(filter);
}
}
const findSpiderman = find((h: Hero) => h.name === "Spiderman");
const spiderman = findSpiderman(heroes);
Functions can also be assigned to variables. For example, in the preceding code snippet, we assigned the function returned by the find function to a variable named findSpiderman:
const findSpiderman = find((h: Hero) => h.name === "SPiderman");
Both JavaScript and TypeScript treat functions as first-class citizens.
Lambda expressions
Lambda expressions are just expressions that can be used to declare anonymous functions (functions without a name). Before the ES6 specification, the only way to assign a function as a value to a variable was to use a function expression:
const log = function(arg: any) { console.log(arg); };
The ES6 specification introduced the arrow function syntax:
const log = (arg: any) => console.log(arg);
Function arity
The arity of a function is the number of arguments that the function takes. A unary function is a function that only takes a single argument:
function isNull<T>(a: T|null) {
return (a === null);
}
Unary functions are very important in functional programming because they facilitate utilization of the function composition pattern.
We will learn more about function composition patterns later in Chapter 6, Functional Programming Techniques.
A binary function is a function that takes two arguments:
function add(a: number, b: number) {
return a + b;
}
Functions with two or more arguments are also important because some of the most common FP patterns and techniques (for example, partial application and currying) have been designed to transform functions that allow multiple arguments into unary functions.
There are also functions with three (ternary functions) or more arguments. However, functions that accept a variable number of arguments, known as variadic functions, are particularly interesting in functional programming, as demonstrated in the following code snippet:
function addMany(...numbers: number[]) {
numbers.reduce((p, c) => p + c, 0);
}
Higher-order functions
A higher-order function is a function that does at least one of the following:
- Takes one or more functions as arguments
- Returns a function as its result
Higher-order functions are some of the most powerful tools that we can use to write JavaScript in a functional programming style. Let's look at some examples.
The following code snippet declares a function named addDelay. The function creates a new function that waits for a given number of milliseconds before printing a message in the console. The function is considered a higher-order function because it returns a function:
function addDelay(msg: string, ms: number) {
return () => {
setTimeout(() => {
console.log(msg);
}, ms);
};
}
const delayedSayHello = addDelay("Hello world!", 500);
delayedSayHello(); // Prints "Hello world!" (after 500 ms)
The following code snippet declares a function named addDelay. The function creates a new function that adds a delay in milliseconds to the execution of another function that is passed as an argument. The function is considered a higher-order function because it takes a function as an argument and returns a function:
function addDelay(func: () => void, ms: number) {
return () => {
setTimeout(() => {
func();
}, ms);
};
}
function sayHello() {
console.log("Hello world!");
}
const delayedSayHello = addDelay(sayHello, 500);
delayedSayHello(); // Prints "Hello world!" (after 500 ms)
Higher-order functions are an effective technique for abstracting a solution for a common problem. The preceding example demonstrates how we can use a higher-order function (addDelay) to add a delay to another function (sayHello). This technique allows us to abstract the delay functionality and keeps the sayHello function, or other functions, agnostic of the implementation details of the delay functionality.
Laziness
Many functional programming languages feature lazy-evaluated APIs. The idea behind lazy evaluation is that operations are not computed until doing so can no longer be postponed. The following example declares a function that allows us to find an element in an array. When the function is invoked, we don't filter the array. Instead, we declare a proxy and a handler:
function lazyFind<T>(arr: T[], filter: (i: T) => boolean): T {
let hero: T | null = null;
const proxy = new Proxy(
{},
{
get: (obj, prop) => {
console.log("Filtering...");
if (!hero) {
hero = arr.find(filter) || null;
}
return hero ? (hero as any)[prop] : null;
}
}
);
return proxy as any;
}
It is only later, when one of the properties in the result is accessed, that the proxy handler is invoked and filtering takes place:
const heroes = [
{
name: "Spiderman",
powers: [
"wall-crawling",
"enhanced strength",
"enhanced speed",
"spider-Sense"
]
},
{
name: "Superman",
powers: [
"flight",
"superhuman strength",
"x-ray vision",
"super-speed"
]
}
];
console.log("A");
const spiderman = lazyFind(heroes, (h) => h.name === "Spiderman");
console.log("B");
console.log(spiderman.name);
console.log("C");
/*
A
B
Filtering...
Spiderman
C
*/
If we examine the console output, we will be able to see that the Filtering... message is not logged into the console until we access the property name of the result object. The preceding implementation is a very rudimentary implementation, but it can help us to understand how lazy evaluation works. Laziness can sometimes improve the overall performance of our applications.
We will learn more about function composition patterns later in Chapter 9, Functional-Reactive Programming.
Summary
In this chapter, we explored some of the most fundamental principles and concepts of the functional programming paradigm.
Over the next four chapters, we are going to deviate a little bit from functional programming because we are going to take an extensive look at functions, asynchronous programming, and certain aspects of the TypeScript/JavaScript runtime, such as closures and prototypes. We need to explore these topics before we can learn more about the implementation of functional programming techniques. However, if you are already very confident with using functions, closures, the this operator, and prototypes, then you should be able to skip the next four chapters.