Functional programming

Since the birth of the JavaScript language, it has the brand of functional programming. It treats functions as an independent data type, and is in a completely equal position with other data types. In the JavaScript language, you can use object-oriented programming or functional programming. Some people even say that JavaScript is the first functional programming language to be adopted on a large scale.

Various new features of ES6 make functional programming more convenient and powerful. This chapter introduces how ES6 performs functional programming.

Currying

Currying refers to splitting a multi-parameter function into a series of functions, and each split function accepts only one parameter (unary).

function add(a, b) {
  return a + b;
}

add(1, 1); // 2

In the above code, the function add accepts two parameters a and b.

Currying is to split the above function into two functions, each of which accepts only one parameter.

function add(a) {
  return function (b) {
    return a + b;
  };
}
// Or use arrow function writing
const add = (x) => (y) => x + y;

const f = add(1);
f(1); // 2

In the above code, the function add only accepts one parameter a and returns a function f. The function f also only accepts one parameter b.

Function synthesis

Function composition refers to combining multiple functions into one function.

const compose = (f) => (g) => (x) => f(g(x));

const f = compose((x) => x * 4)((x) => x + 3);
f(2); // 20

In the above code, compose is a function synthesizer, used to synthesize two functions into one function.

It can be found that currying is closely related to function composition. The former is used to split a function into multiple functions, and the latter is used to merge multiple functions into one function.

Parameter inversion

Parameter flip refers to changing the order of the first two parameters of a function.

var divide = (a, b) => a / b;
var flip = f.flip(divide);

flip(10, 5); // 0.5
flip(1, 10); // 10

var three = (a, b, c) => [a, b, c];
var flip = f.flip(three);
flip(1, 2, 3); // => [2, 1, 3]

In the above code, if you follow the normal parameter order, 10 divided by 5 equals 2. However, the new function obtained after the parameters are inverted, the result is 5 divided by 10, and the result is 0.5. If the original function has 3 parameters, only the positions of the first two parameters are reversed.

The code for parameter inversion is very simple.

let f = {};
f.flip =
  (fn) =>
  (a, b, ...args) =>
    fn(b, a, ...args.reverse());

Execution boundary

The execution boundary (until) refers to the function execution until the condition is met.

let condition = (x) => x > 100;
let inc = (x) => x + 1;
let until = f.until(condition, inc);

until(0); // 101

condition = (x) => x === 5;
until = f.until(condition, inc);

until(3); // 5

In the above code, the condition of the first paragraph is to execute until x is greater than 100, so when the initial value of x is 0, it will be executed until 101. The condition of the second paragraph is to execute until it is equal to 5, so the final value of x is 5.

The implementation of the execution boundary is as follows.

let f = {};
f.until =
  (condition, f) =>
  (...args) => {
    var r = f.apply(null, args);
    return condition(r) ? r : f.until(condition, f)(r);
  };

The key to the above code is to return the result if the condition is met, otherwise it will continue to execute recursively.

Queue operation

Queue (list) operations include the following.

-head: Remove the first non-empty member of the queue. -last: Remove the last non-empty member of the limited queue. -tail: Take out other non-empty members except "head of queue". -init: Take out other non-empty members except "end of queue".

The following is an example.

f.head(5, 27, 3, 1); // 5
f.last(5, 27, 3, 1); // 1
f.tail(5, 27, 3, 1); // [27, 3, 1]
f.init(5, 27, 3, 1); // [5, 27, 3]

The implementation of these methods is as follows.

let f = {};
f.head = (...xs) => xs[0];
f.last = (...xs) => xs.slice(-1);
f.tail = (...xs) => Array.prototype.slice.call(xs, 1);
f.init = (...xs) => xs.slice(0, -1);

Merge operation

The merge operation is divided into two types: concat and concatMap. The former is to combine multiple arrays into one, and the latter is to process the parameters first, and then combine the processing results into an array.

f.concat([5], [27], [3]); // [5, 27, 3]
f.concatMap((x) => "hi " + x, 1, [[2]], 3); // ['hi 1','hi 2','hi 3']

The implementation code of these two methods is as follows.

let f = {};
f.concat = (...xs) => xs.reduce((a, b) => a.concat(b));
f.concatMap = (f, ...xs) => f.concat(xs.map(f));

Pairing operation

The pairing operation is divided into two methods: zip and zipWith. The zip operation pairs the members of the two queues one by one to form a new queue. If the two queues are not of equal length, the extra members of the longer queue will be ignored. The first parameter of the zipWith operation is a function, and then the following queue members will be paired one by one, input the function, and the return value will form a new queue.

The following is an example.

let a = [0, 1, 2];
let b = [3, 4, 5];
let c = [6, 7, 8];

f.zip(a, b); // [[0, 3], [1, 4], [2, 5]]
f.zipWith((a, b) => a + b, a, b, c); // [9, 12, 15]

In the above code, the first parameter of the zipWith method is a summation function, which adds up the members of the following three queues one by one.

The implementation of these two methods is as follows.

let f = {};

f.zip = (...xs) => {
  let r = [];
  let nple = [];
  let length = Math.min.apply(
    null,
    xs.map((x) => x.length)
  );

  for (var i = 0; i < length; i++) {
    xs.forEach((x) => nple.push(x[i]));

    r.push(nple);
    nple = [];
  }

  return r;
};

f.zipWith = (op, ...xs) => f.zip.apply(null, xs).map((x) => x.reduce(op));