Functional programming, as the name implies, is about functions. While functions are part of just about every programming paradigm, including JavaScript, a functional programmer has unique considerations to approach and use functions.

This tutorial explains what is different and special about a functional programmer's approach, and demonstrates how to implement JavaScript functions from a functional programming point of view. It assumes the reader has experience programming in JavaScript.

Functions as a map between two sets

A functional programmer views the nature of a function in a similar way as does a mathematician: It's a map between two sets of values.

For example, imagine a scenario in which a value is described by the variable x. Next, imagine a function named f that performs an operation on the variable x. Finally, imagine a variable y that is assigned the result of applying the operation defined by the function f to the variable x. The following expression describes this scenario:

y = f(x)

Now, imagine that the behavior in function f is to square the value of x. If we apply the values 1, 2, 3, 4 and 5 to the function f, the result will always be 1, 4, 9, 16 and 25, as illustrated by the following:

f = (x * x)

1  |  f(1)  |  1

2  |  f(2)  |  4

3  |  f(3)  |  9

4  |  f(4)  |  16

5  |  f(5)  |  25

Thus, we can say that the function f(x) is a named map between the values (1, 2, 3, 4, 5) and (1, 4, 9, 16, 25), like so:

Set A|          | Set B |

|-----|          |-------| 

| 1  |          |   1   |

| 2  |          |   4   |

| 3  |  f(x) -> |   9   |

| 4  |          |   16  |

| 5  |          |   25  |

We also can say that given Set A, f(x) will always produce the values in Set B and no other values.

How functions become functional programming

All this talk about f(x) being a map between sets of values isn't just an esoteric exercise -- it's directly applicable to the work of day-to-day programmers.

The next JavaScript code snippet illustrates how the map() function, which is an implicit part of a JavaScript array, applies the concept of function-as-map.

const arr = [1, 2, 3, 4, 5];

const f = x => x * x;

const y = arr.map(f);

console.log(arr); // Output: [1, 2, 3, 4, 5]

console.log(y); // Output: [1, 4, 9, 16, 25]

console.log(arr); // Output: [1, 2, 3, 4, 5]

In that code, the variable arr is an array, and the variable f declares a function that returns the square of the value x. Thus, calling the function arr.map(f) applies the squaring function (as defined by the variable f) to each element in the array arr, and a new array that contains the squared value of each element in the array named arr is assigned to the value variable named y.

The previous code example illustrates four important points about functional programming:

  • We can indeed think of the function f as a map between two sets of numbers. In terms of arr.map(f) where f is the squaring functions, for any value x in the array arr there is only one corresponding value y. The console output noted previously demonstrates this notion.
  • The .map() function does not affect the value of any element in the array arr. Rather, the arr.map(f) returns a new array that is assigned the variable y. This new array has the squared values. Importantly, the .map() function treats the values of the elements in the array arr as immutable. Data immutability is a fundamental principle of functional programming.
  • The function f produces no side effects; it only squares a number provided as input. All behavior takes place within the function and only within the function. Producing no side effects is another important principle of functional programming.
  • The JavaScript method Array.prototype.map() is a higher-order function because it accepts a function as an argument. Given a pure callback and an unchanged input array, it can be used in a pure functional style.

That last point about pure functions warrants further exploration.

First-order, higher-order and pure functions

Two different classifications are useful here. First-order and higher-order describe how functions interact with other functions. Pure describes a function\'s behavior. These concepts are independent: a first-order or higher-order function can be pure or impure.

First-order functions

A first-order function does not accept functions as arguments or return a function as its result. It can still accept and return ordinary JavaScript values such as strings, numbers, arrays and objects.

Functional programming favors pure functions and minimizes uncontrolled side effects. For example, imagine an impure capitalize() operation that changes state outside the function rather than returning a transformed value:

const message = { text: "Hi There" };

const capitalize = value => {
    value.text = value.text.toUpperCase();
};

capitalize(message);

console.log(message.text); // Output: HI THERE

The capitalize() function mutates the object supplied by its caller. That externally visible mutation is a side effect and makes the function harder to reason about in a functional style.

However, if the function capitalize() were to return a new capitalized string based on the value of the string passed as a parameter to the function, as shown in the following, this would incur no side effect.

const myString = "Hi There";

const capitalize = value => value.toUpperCase();
const result = capitalize(myString);

console.log(result);   // Output: HI THERE
console.log(myString); // Output: Hi There

The next code sample demonstrates a first-order function named getEmployeeSalary(employeeName, employees). The function takes two parameters, each of which is a standard data type (string, array), and the function returns another standard data type, a number.

const employees = {

    "Moe Howard": 125000,

    "Larry Fine": 100000,

    "Curly Howard": 85000,

    "Shemp Howard": 75000,

    "Joe Besser": 70000,

    "Joe DeRita": 65000

}

const getEmployeeSalary = (employeeName, employees) => {

    if(employeeName in employees) {

        return employees[employeeName];

    }

}

console.log(getEmployeeSalary("Moe Howard", employees)); // Output: 125000

Higher-order and pure functions

A higher-order function accepts one or more functions as arguments, returns a function, or both. Purity is a separate property. A pure function is deterministic for the same relevant inputs and does not produce observable side effects.

In addition, a pure function supports the following principles:

  • It is deterministic. Given the same input, it always produces the same output.
  • It does not produce side effects. It doesn't modify any external state (variables, objects, files, etc.) outside of its own scope.
  • It has referential transparency. Any expression that uses a pure function can be replaced with the function's return value without affecting the program's behavior.

The following code shows a scenario in JavaScript that uses a pure function created as an anonymous function assigned to the variable payEmployee to facilitate paying an employee. The function takes the following parameters:

  • employeeName, a string.
  • employees, an object of salary according to employee name.
  • salaryFunc, a function that returns the employee salary.
  • taxRateFunc, a function that returns the taxRate according to salary.
  • frequency, an enum that reports the pay day frequency.
// create an object that has a salary according to the employee

const employees = {

    "Moe Howard": 125000,

    "Larry Fine": 100000,

    "Curly Howard": 85000,

    "Shemp Howard": 75000,

    "Joe Besser": 70000,

    "Joe DeRita": 65000

}

// create an enum that describes the pay day frequency

const PaymentFrequency = Object.freeze({

    WEEKLY: 'WEEKLY',

    BIWEEKLY: 'BIWEEKLY',

    MONTHLY: 'MONTHLY'

});

// get the employee salary from the the employees object

const getEmployeeSalary = (employeeName, employees) => {

    if (!(employeeName in employees)) {

        return "Employee not found";

    }

    return employees[employeeName];

};

// get the income tax rate according to yearly salary

function getIncomeTaxRate(yearlySalary) {

    // 2024 tax brackets for single filers

    const taxBrackets = [

        {min: 0, max: 11600, rate: 0.10},

        {min: 11601, max: 47150, rate: 0.12},

        {min: 47151, max: 100525, rate: 0.22},

        {min: 100526, max: 191950, rate: 0.24},

        {min: 191951, max: 243725, rate: 0.32},

        {min: 243726, max: 609350, rate: 0.35},

        {min: 609351, max: Infinity, rate: 0.37}

    ];

    // Find the appropriate tax bracket

    const bracket = taxBrackets.find(bracket => yearlySalary >= bracket.min && yearlySalary <= bracket.max);

    if (bracket) {

        // Return the tax rate as a percentage

        return bracket.rate * 100;

    } else {

        return "Invalid salary input";

    }

}

// figure out the employee's pay, according to payment frequency

const payEmployee = (employeeName, employees, salaryFunc, taxRateFunc, frequency) => {

    const salary = salaryFunc(employeeName, employees);

    const taxRate = taxRateFunc(salary);

    let netSalary = salary - (salary * (taxRate / 100));

    if (frequency === PaymentFrequency.WEEKLY) {

        netSalary /= 52;

    } else if (frequency === PaymentFrequency.BIWEEKLY) {

        netSalary /= 26;

    } else if (frequency === PaymentFrequency.MONTHLY) {

        netSalary /= 12;

    }

    return netSalary;

};

// pay the employee named "Moe Howard"

const pay = payEmployee("Moe Howard", employees, getEmployeeSalary, getIncomeTaxRate, PaymentFrequency.WEEKLY);

console.log(Math.round(pay * 100) / 100); // Output: 1826.92

In that example, payEmployee uses functions passed as arguments to separate salary lookup from tax-rate calculation. The functions read their inputs and return values rather than deliberately mutating the supplied data. JavaScript does not automatically make function arguments immutable, so avoiding mutation is a design choice that the code must preserve.

Encapsulating logical concerns into discrete functions makes it easier to debug and refactor. There's no spaghetti code to pore over. Each function is executable in its own right.

But look more closely -- there's a problem. In the following getEmployeeSalary function, notice that if there is an error, the function returns the string "Employee not found."

const getEmployeeSalary = (employeeName, employees) => {

    if (!(employeeName in employees)) {

        return "Employee not found";

    }

    return employees[employeeName];

}

While this makes sense from an operational point of view, it violates the spirit of a pure function -- the expected data type of the function's result is a number, yet a string is returned to report an error. (Functional programming frowns upon ambiguous return types.) Moreover, throwing an error within the function will create a global side effect that might impact the overall well-being of the program.

So, what's to be done? The answer is to use a monad.

Handling errors with monads

A monad in functional programming is a construct that provides a way to sequence functions in a structured and composable manner. In terms of error handling, a monad provides a way to interact consistently with values returned from a function, even in the event of a runtime mishap.

The following JavaScript code implements a small Maybe-style container. It is intentionally simplified for teaching purposes rather than a complete general-purpose monad library.

// A simple Maybe monad

const Maybe = {

    OK: (value) => ({

        map: (f) => Maybe.OK(f(value)),

        flatMap: (f) => f(value),

        getOrElse: () => value,

    }),

    Nothing: () => ({

        map: () => Maybe.Nothing(),

        flatMap: () => Maybe.Nothing(),

        getOrElse: (defaultValue) => defaultValue,

    }),

};

There's a lot going on in the monad in terms of how logic is implemented in a JavaScript object. Explaining the details of the Maybe monad is a bit beyond the scope of this tutorial, but for now it is important to understand that the monad can be used to accommodate runtime error handling in a structured, composable manner.

Take a look at the next use of the Maybe monad in the function named getEmployeeSalary.

const getEmployeeSalary = (employeeName, employees) =>

    employeeName in employees

        ? Maybe.OK({ employeeName, salary: employees[employeeName], status: "Known"})

        : Maybe.Nothing();

Here's how that works: If the employeeName passed as a parameter to the function is in the employees object that is also passed into the function as a parameter, the monad's Maybe.OK() function will be called with an object with the following properties:

{
    employeeName,
    salary: employees[employeeName],
    status: "Known"
}

If the employee name is not in the employees object, the Maybe.Nothing() function will be called. However, implementing getEmployeeSalary() will be expressed using the Maybe.getOrElse() function as follows:

let employeeName = "Moe Howard";

const salary = getEmployeeSalary(employeeName, employees).getOrElse({ employeeName, salary: 0, status: "Unknown"}));

Thus, if getEmployeeSalary() is successful, the return will be as follows:

{  employeeName: 'Moe Howard', salary: 125000, status: 'Known'  }

However, if the employee is unknown, like so:

employeeName = "John Doe";

The return will be as follows:

{ employeeName: 'John Doe', salary: 0, status: 'Unknown' }

Here, getOrElse() does not catch an exception. Instead, it unwraps the value stored in Maybe.OK, or returns the caller-supplied fallback when the computation produced Maybe.Nothing().

So, in the following case:

const salary = getEmployeeSalary(employeeName, employees).getOrElse({ employeeName, salary: 0, status: "Unknown"}));

The programmer has made it so that .getOrElse() returns data in the same format that's also defined in the Maybe.OK() declaration of getEmployeeSalary(). Like so:

const getEmployeeSalary = (employeeName, employees) =>

    employeeName in employees

        ? Maybe.OK({ employeeName, salary: employees[employeeName], status: "Known"})

        : Maybe.Nothing();

Granted, we're introducing a bit of advanced function design using monad and composition in terms of using JavaScript as a functional programming language. The important thing to understand in using the Maybe monad is that error handling behavior is implemented outside the function, and thus controllable by the developer composing the function chain. No side effect is incurred.

Putting it all together

One of the benefits of JavaScript is that it's a versatile programming language. You can use it with HTML to put logic in webpages. You can also use it to drive a web server using Node.js. And, as you've seen, you can use JavaScript to do functional programming.

Effective functional JavaScript emphasizes small pure functions, immutable transformations, explicit data flow and composition. Monadic or result-style containers can be useful for optional values and errors, but they are one technique among several, not a requirement for functional programming.

Hopefully, the concepts and examples presented here will provide the basic understanding you'll need to adapt your knowledge of JavaScript to functional programming.

Bob Reselman is a software developer, system architect and writer. His expertise ranges from software development technologies to techniques and culture.