Discover Functional Programming in JavaScript with this thorough introduction

155 阅读8分钟

Discover Functional Programming in JavaScript with this thorough introduction

Go to the profile of Cristi Salcescu
Cristi SalcescuBlockedUnblockFollowFollowing
Photo by John Schnobrich on Unsplash

JavaScript is the first language to bring Functional Programming to the mainstream. It has first-class functions and closures. They open the way for functional programming patterns.

First-Class Functions

Functions are first-class objects. Functions can be stored in variables, objects or arrays, passed as arguments to other functions or returned from functions.

//stored in variablefunction doSomething(){}
//stored in variableconst doSomething = function (){ };
//stored in propertyconst obj = {    doSomething : function(){ } }
//passed as an argumentprocess(doSomething);
//returned from functionfunction createGenerator(){  return function(){  }}

Lambdas

A lambda is a function that is used as a value.

In JavaScript, functions are first-class objects, so all functions can be used as values. All functions can be lambdas with or without a name. I actually suggest favoring named functions.

Functional Array Toolbox

Basic Toolbox

filter() selects values from a list based on a predicate function that decides what values should be kept.

const numbers = [1,2,3,4,5,6];
function isEven(number){  return number % 2 === 0;}
const evenNumbers = numbers.filter(isEven);

A predicate function is a function that takes one value as input and returns true/false based on whether the value satisfies the condition. isEven() is a predicate function.

map() transforms a list of values to another list of values using a mapping function.

function toCodeOfLength(number){  return "a".repeat(number);}
const codes = numbers.map(toCodeOfLength);

reduce() reduces a list of values to one value. There is a reduceRight() method that starts from the last value in the list.

function addNumber(total, number){  return total + number;}
const sum = numbers.reduce(addNumber, 0);

Extended Toolbox

find() returns the first value that satisfies the predicate function.

findIndex() returns the index of the first value that satisfies the predicate function.

some() checks if at least one value in the list passes the test implemented by the predicate function.

every() checks if all values in the list pass the test implemented by the predicate function.

const firstEven = numbers.find(isEven);const firstEvenIndex = numbers.findIndex(isEven);const areAllEven = numbers.every(isEven);const hasOneEven = numbers.some(isEven);

forEach() calls the callback function once for each element in an array, in order.

function log(value){  console.log(value);}
numbers.forEach(log);

sort() sorts the elements of an array. It is an impure method, it modifies the input array.

function ascByUserName(todo1, todo2){  return todo1.userName.localeCompare(todo2.userName);}
todos.sort(ascByUserName);

Intention revealing names

Functions can be created with or without a name. The arrow syntax usually creates anonymous functions. Consider the following code written with an anonymous arrow function:

numbers.filter(number => number % 2 === 0);

I suggest to always use named functions whenever the name adds value to the existing code:

function isEven(number){  return number % 2 === 0;}
numbers.filter(isEven);

Intention revealing names improve readability and understanding of the code, make a better debugging experience and also offer the option for self-reference — used by recurring functions.

For more on intention revealing names, take a look at How to make your code better with intention-revealing function names.

Point-free style

Point-free is a technique that improves readability by eliminating the unnecessary arguments.

Consider the next code:

todos.filter(todo => isPriorityTodo(todo))     .map(todo => toTodoViewModel(todo));

In a point-free style, the same code is written without arguments:

todos.filter(isPriorityTodo).map(toTodoViewModel);

For more on point-free code, look at How point-free composition will make you a better functional programmer.

Closures

Closure is an inner function that has access to the outer scope, even after the outer scope has closed.

Closure becomes important when the inner function survives the execution of the outer scope. This happens in the following situations:

  • The inner function is used as a callback for an asynchronous task like a timer, an event, or a network call
  • The parent function returns the inner function or returns an object storing the inner function

Consider the next example:

(function autorun(){    let x = 1;    $("#btn").on("click", function log(){      console.log(x);    });})();

The log() function survives the invocation of the autorun() parent function. log()is a closure.

The variable x, used in an event handler, lives forever or until the handler is removed.

Generators

A generator is a function that returns a new value from the sequence, every time it is called.

In the next example, the integer() factory will create a generator that returns the next integer in the sequence, each time it is called. At the end of the sequence it returns undefined:

function integer(from, to){ let count = from; return function(){   if(count< to){      const result = count;      count += 1;      return result;    }  }}

With closures, we can create functions with a private state. The returned generator is a function with private state.

We can then isolate the loop statement in one function. The repeat() function gets a generator and a callback as inputs and runs the callback until the generator returns undefined.

function repeat(generate, callback){  let i = generate();  while(i !== undefined){     callback(i);     i = generate();  } }

The same logic can be implemented with a tail recursive function:

function repeat(generate, callback){  const i = generate();  if (i !== undefined){      callback(i);      return repeat(generate, callback);    }}
A recursive function is tail recursive when the recursive call is the last thing the function does.

The tail recursive functions are better than non tail recursive functions. Tail-recursion can be optimized by a compiler.

In the next example, the log() function is called for all numbers returned by the generator.

function log(x){   console.log(x);}
repeat(integer(0, 10), log);

Having the array toolbox at our disposal and by creating new generators we can replace the use of loop statements (for/while/do-while) and write the code in a more expressive way. There is no need to use the for-in statement, we can use Object.keys() and then use the array methods.

Higher-order functions

A higher order function is a function that takes another function as an input, returns a function, or does both.

All functions from the array toolbox are higher-order functions as they get functions as inputs.

Decorators

A function decorator is a higher-order function that takes one function as an argument and returns another function, and the returned function is a variation of the argument function — Javascript Allongé

unary()

unary() is a function decorator that takes a function and returns a function taking one parameter. It’s usually used to fix problems when the function is called with more arguments than necessary.

function unary(fn){  return function(first){     return fn.call(this, first);  }}

Look at the next examples using the unary() function.

const numbers = [1,2,3,4,5,6];
numbers.forEach(unary(console.log));//1 2 3 4 5 6numbers.map(unary(parseInt)); //[1, 2, 3, 4, 5, 6]

before()

The before(count, fn) decorator limits access to the original function. The original function can be called no more than count times. After that, the result of the last call is returned.

function before(count, fn){   let runCount = 0;   return function runAfter(){      runCount = runCount + 1;      if (runCount <= count) {         return fn.apply(this, arguments);              }   }}
function log(){ console.log("process"); }
const limitedLog = before(2, log);limitedLog(); //processlimitedLog(); //processlimitedLog();

You can find other common decorators in libraries like underscore.js, lodash.js or ramda.js:

  • once(fn): creates a version of the function that executes only once. It’s useful for an initialization function, where we want to make sure it runs only once, no matter how many times it is called from different places.
  • throttle(fn, wait): creates a version of the function that, when invoked repeatedly, will call the original function once per every wait milliseconds. It’s useful for limiting events that occur faster.
  • debounce(fn, wait): creates a version of the function that, when invoked repeatedly, will call the original function after wait milliseconds since the last invocation. It’s useful for running a function only after the event has stopped arriving.
  • memoize(fn): Memoizes a given function by saving the computed result. It’s useful for speeding up slow computations. The memoized function should be a pure function.

Function decorators are a powerful tool for creating variations of existing functions without modifying the original functions. Function decorators use closures.

For more on decorators, check out Here are a few function decorators you can write from scratch and How to use Decorators with Factory Functions.

Partial Application

Partial application refers to the process of fixing a number of parameters by creating a new function with fewer parameters than the original.

A partial application can be used when aiming for pure functions. Consider the next code:

function getBy(query) {   return todos.filter(todo => {     if(query && query.text){        return todo.title.includes(query.text);     }     return true;   });}

With a partial application, we can refactor out the predicate function into a pure function with an intention revealing name. See the code below:

function byQuery(query, todo){   if(query && query.text){      return todo.title.includes(query.text);   }   return true;}     function getBy(query) {   return todos.filter(partial(byQuery, query));}

The partial() function can be found in popular libraries like underscore.js or lodash.js.

Curring

Currying is the process of transforming a function with many parameters into a series of functions that each take a single parameter.

Let’s take the previous situation, where byQuery() has two parameters and we want to use it with todos.filter() which only sends the current todo.

See below how the query parameter can be sent after the byQuery function was curried:

let byQuery = curry(function byQuery(query, todo){   if(query && query.text){      return todo.title.includes(query.text);   }   return true;});    function getBy(query) {   return todos.filter(byQuery(query));}

The curry() function can be found in the lodash.js library for example.

Function Composition

Function composition is applying one function to the result of another.

Applying f() to the result of g() means compose(f,g)(x) and is the same as f(g(x)).

Composition works best with unary functions.

Decorators are unary functions. If for example, we have two decorators authorize() and logging(), applying both decorators on the same function will mean: authorize(logging(fn)) or compose(authorize,logging)(fn).

The compose() function can be found in libraries like underscore.js.

For more details on how to use function composition with decorators, take a look at How to use Decorators with Factory Functions.

Immutability

An immutable object is an object that, once created, cannot be changed.

Object.freeze() can be used to freeze objects. Properties can’t be added, deleted, or changed. The object becomes immutable.

const book = Object.freeze({  title : "How JavaScript Works",  author : "Douglas Crockford"});
book.title = "Other title";//Cannot assign to read only property 'title'

Object.freeze() does a shallow freeze. The nested objects can be changed. For deep freeze, recursively freeze each property of type object. Check the deepFreeze() implementation.

If we want to modify an immutable object, we need to create a new object. For example, if we want to change the title, we create a new object:

const newBook = Object.freeze(Object.assign({}, book,{ title: "A new title"}));

To avoid any unexpected change, which will be hard to understand, all the data transfer objects inside the application should be immutable.

Pure Functions

A pure function is a function that returns a value based only on its input. Given a specific input, it always returns the same output. Pure functions cause no mutations. It does not use variables from outside its scope. It does not modify its input parameters. It doesn’t do anything else other than returning a value.

Pure functions have no side-effects. A side-effect is anything else that happens in the function beside computing the result.

Given all these, pure functions have a big set of advantages. They are easier to read and understand, as they do one thing. There is no need to look outside the function code and check for variables. There is no need to think how changing the value of the variable will affect other functions. No mutations in other functions will affect the result of the pure function for a specific input.

Pure functions are easier to test, as all dependencies are in the function definition and they do one thing.

Pure functions are excellent candidates for parallelization, when that will be available.

We should favor pure functions.

Promises

A promise is a reference to an asynchronous call. It may resolve or fail somewhere in the future.

Promises are easier to combine. As seen in the next example, it is easy to call a method when all promises are resolved, or when at least one promise is resolved.

function getTodos() { return fetch("/todos"); }function getUsers() { return fetch("/users"); }function getAlbums(){ return fetch("/albums"); }
const getPromises = [  getTodos(),   getUsers(),   getAlbums()];
Promise.all(getPromises).then(doSomethingWhenAll);Promise.race(getPromises).then(doSomethingWhenOne);
function doSomethingWhenAll(){}function doSomethingWhenOne(){}

Promises support a chaining system that allows passing data through a set of functions. In the next example, the result of getTodos()passes as input to toJson(). Then its result is passed as input to getTopPriority(). Then its result is passed as input to renderTodos() function:

getTodos()  .then(toJson)  .then(getTopPriority)  .then(renderTodos)  .catch(handleError);
function toJson(response){}function getTopPriority(todos){}function renderTodos(todos){}function handleError(error){}

Recursion

Recursion is a method of solving a problem where the solution is found by solving smaller instances of the same problem. Recursion implies that the function calls itself.

The recursion needs a termination. It happens when certain conditions are met and the recursive algorithm stops calling itself and returns a value.

In the next example, recursion is used to traverse a tree and create an array with top selected node values.

const tree = {      value: 0,      checked: false,      children: [{              value: 1,              checked: true,              children: [{                  value: 11,                  checked: true,                  children: null                }]          }, {              value: 2,              checked: false,              children: [{                  value: 22,                  checked: true,                  children: null                }]         }]}
function getTopSelection(node){    if(node.checked){       return [node.value];    }             if(!node.checked && node.children) {       let selection = [];       node.children.forEach(function add(childNode){           selection = selection.concat(getTopSelection(childNode));       });       return selection;    }}

Functional Programming

A program is said to be functional when it uses concepts of functional programming, such as first-class functions, closures, higher-order functions, immutability.

Pure Functional Programming

Pure Functional Programming is a programming paradigm that treats all functions as mathematical functions.

In Pure Functional Programming, all functions are pure functions.

JavaScript is not a pure functional language, however pure functions can be written with code discipline:

  • No use of the assignment operator
  • Declare variable only with const. No use of let, var statements.
  • Only use array pure methods
  • No use of for statement
  • No use of Math.random() or Date.now() as they give different values on each call

Not 100% Pure

Applications can’t be 100% pure. The network calls are not pure. Rendering and interacting with the UI are not pure. There is state change.

We should separate pure from impure code, and encapsulate the impure code.

Objects are valuable for encapsulating and managing state.

Conclusion

JavaScript is the first functional language to go mainstream.

Functions are first-class. They can be used as values.

filter(), map(), reduce() are the basic toolbox for working with collections in a functional style.

Point-free improves readability by eliminating the unnecessary arguments.

Decorators create variations of existing functions.

Pure functions return a value based only of its input. Pure functions are easier to read and understand.

For more on the JavaScript functional side take a look at:

Discover the power of closures in JavaScript

Discover the power of first class functions

How point-free composition will make you a better functional programmer

How to make your code better with intention-revealing function names

Here are a few function decorators you can write from scratch

Make your code easier to read with Functional Programming