Getty Images/iStockphoto
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.
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 variablex
. Next, imagine a function namedf
that performs an operation on the variablex
. Finally, imagine a variabley
that is assigned the result of applying the operation defined by the functionf
to the variablex
. The following expression describes this scenario:
y = f(x)
Now, imagine that the behavior in functionf
is to square the value ofx
. If we apply the values 1, 2, 3, 4 and 5 to the functionf
, the result will always be 1, 4, 9, 16 and 25, as illustrated by the following:
f = (x * x)1 | f(1) | 12 | f(2) | 43 | f(3) | 94 | f(4) | 165 | f(5) | 25
Thus, we can say that the functionf(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.
All this talk aboutf(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 themap()
function, which is an implicit part of a JavaScript array, applies the concept offunction-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 variablearr
is an array, and the variablef
declares a function that returns the square of the valuex
. Thus, calling the functionarr.map(f)
applies the squaring function (as defined by the variablef
) to each element in the arrayarr
, and a new array that contains the squared value of each element in the array namedarr
is assigned to the value variable namedy
.
The previous code example illustrates four important points about functional programming:
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..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 fundamentalprinciple of functional programming.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.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.
In functional programming, there are two varieties of functions:first-order functions andpure 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, thecapitalize()
function is causing a side effect: The value ofmystring
, which is declared outside of the function, is being altered. This causes a violation of the principle of no side effects.
However, if the functioncapitalize()
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 namedgetEmployeeSalary(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
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:
The following code shows a scenario in JavaScript that uses a pure function created as an anonymous function assigned to the variablepayEmployee
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
, anenum that reports the pay day frequency.// create an object that has a salary according to the employeeconst 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 frequencyconst PaymentFrequency = Object.freeze({ WEEKLY: 'WEEKLY', BIWEEKLY: 'BIWEEKLY', MONTHLY: 'MONTHLY'});// get the employee salary from the the employees objectconst getEmployeeSalary = (employeeName, employees) => { if (!(employeeName in employees)) { return "Employee not found"; } return employees[employeeName]}// get the income tax rate according to yearly salaryfunction 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 frequencyconst 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 functionpayEmployee
is acomposed 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 andrefactor. There's nospaghetti code to pore over. Each function is executable in its own right.
But look more closely -- there's a problem. In the followinggetEmployeeSalary
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.
Amonad 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 namedMaybe
.
// A simple Maybe monadconst 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 theMaybe
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 accommodateruntime error handling in a structured, composable manner.
Take a look at the next use of theMaybe
monad in the function namedgetEmployeeSalary
.
const getEmployeeSalary = (employeeName, employees) => employeeName in employees ? Maybe.OK({ employeeName, salary: employees[employeeName], status: "Known"}) : Maybe.Nothing();
Here's how that works: If theemployeeName
passed as a parameter to the function is in theemployees
object that is also passed into the function as a parameter, the monad'sMaybe.OK()
function will be called with an object with the following properties:
{ ,employeeName, .salary, .status}
If theemployee
name is not in theemployees
object, theMaybe.Nothing()
function will be called. However, implementinggetEmployeeSalary()
will be expressed using theMaybe.getOrElse()
function as follows:
let employeeName = "Moe Howard";const salary = getEmployeeSalary(employeeName, employees).getOrElse({ employeeName, salary: 0, status: "Unknown"}));
Thus, ifgetEmployeeSalary()
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, theMaybe.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 toMaybe.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 theMaybe.OK()
declaration ofgetEmployeeSalary()
. 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.
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.
An ADR is only as good as the record quality. Follow these best practices to establish a dependable ADR creation and maintenance ...
At some point, all developers must decide whether to refactor code or rewrite it. Base this choice on factors such as ...
API proxies and gateways help APIs talk to applications, but it can be tricky to understand vendor language around different ...
A principal engineer says Postman's Spec Hub will help the company shift to a spec-first API development process for its ...
AWS Kiro, a project developed by a 'small, opinionated team within AWS,' prioritizes spec-driven development in the workflows of ...
Docker is expanding the Docker Compose spec to accommodate AI agents in an effort to bring AI development closer to existing ...
AI is transforming PaaS with automation and cost-efficient features, but will it eventually replace cloud platforms? Industry ...
Even though Q-Day might be several years away, enterprises should develop a strategic plan to prepare for the future. Experts ...
Businesses can find security vulnerabilities when they push their workloads to the edge. Discover the pitfalls of cloud edge ...
Runtime security and tokenization stand to play a bigger role in attack surface management, a development that could influence ...
Check out the latest security news from the Informa TechTarget team.
How CISOs design and build their security teams is as important as the technology they select to safeguard their organizations' ...
Compare Datadog vs. New Relic capabilities including alerts, log management, incident management and more. Learn which tool is ...
Many organizations struggle to manage their vast collection of AWS accounts, but Control Tower can help. The service automates ...
There are several important variables within the Amazon EKS pricing model. Dig into the numbers to ensure you deploy the service ...