Getty Images/iStockphoto
A JavaScript functional programming basic tutorial
JavaScript's versatility makes it useful for webpages and web servers, but also functional programming. This tutorial shows how to implement JavaScript functional programming.
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 ofarr.map(f)
wheref
is the squaring functions, for any valuex
in the arrayarr
there is only one corresponding valuey
. The console output noted previously demonstrates this notion. - The
.map()
function does not affect the value of any element in the arrayarr
. Rather, thearr.map(f)
returns a new array that is assigned the variabley
. This new array has the squared values. Importantly, the.map()
function treats the values of the elements in the arrayarr
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.map(f)
is a pure function in that it takes a function as a parameter.
That last point about pure functions warrants further exploration.
First-order functions vs. pure functions
In functional programming, there are two varieties of functions: first-order functions and pure functions.
First-order functions
A first-order function takes standard data types (string, number, boolean, array, object) as parameters and returns any of these standard data types too. Also, a first-order function must return a value.
In functional programming, a function cannot cause a side effect. Thus, if you have a function -- for example, capitalize(string)
-- that does not return the value, the only value that can be capitalized is the string passed into the function as a parameter and it is the parameter value that is affected, like so:
const mystring = "Hi There"
capitalize(mystring)
console.log(mystring) // result is HI THERE
Clearly, the capitalize()
function is causing a side effect: The value of mystring
, which is declared outside of the function, is being altered. This causes a violation of the principle of no side effects.
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 str = capitalize(mystring)
console.log(str) // result is 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
Pure functions
A pure function can take standard data types as parameters, as does a first-order function, but it also can take a function as a parameter. In addition, a pure function can return a function as a result or (as with first-order functions) a standard data type.
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
As you can see in that example, the function payEmployee
is a composed function, made up of logic encapsulated in the functions that are passed in as parameters. Each of those parameter functions has a specific concern. Also, each parameter function returns its own data and incurs no side effects. No global data is affected. The data passed into a function is immutable.
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 is an example of a monad named Maybe
.
// 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,
.status
}
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' }
Semantically, the Maybe.getOrElse()
function will return the correct answer from a function call according to a particular format defined within the called function. But, if there is an exception, the return of Maybe.getOrElse()
will be reported according to the data structure provided by the call to Maybe.getOrElse({...})
when the host function is executed.
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.
The trick to effective functional programming with JavaScript is to think like a functional programmer. This means writing pure functions that are deterministic, referentially transparent and create no side effects. It also means one must handle function exceptions using a monad, which is no small undertaking and will take time to master.
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.