Movatterモバイル変換


[0]ホーム

URL:


TheServerSideTheServerSideTechTarget

Getty Images/iStockphoto

Tip

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.

Bob Reselman
By
Published:19 Dec 2024

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 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.

How functions become functional programming

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:

  • We can indeed think of the functionf 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 fundamentalprinciple of functional programming.
  • The functionf 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 andpure 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, 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

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 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.

Handling errors with monads

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.

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.

Dig Deeper on Core Java APIs and programming techniques

Sponsored News
Related Content
SearchAppArchitecture
SearchSoftwareQuality
SearchCloudComputing
SearchSecurity
SearchAWS
Close

[8]ページ先頭

©2009-2025 Movatter.jp