JavaScript не является функциональным языком программирования, как Lisp или Haskell, но тот факт, что JavaScript может манипулировать функциями как объектами, означает, что мы можем использовать методы функционального программирования в JavaScript. Методы работы с массивами, такие как map() и reduce(), особенно хорошо подходят для функционального стиля программирования.
Обработка массивов с помощью функций
Предположим, у нас есть массив чисел, и мы хотим вычислить среднее и стандартное отклонение этих значений. Мы можем сделать это в нефункциональном стиле следующим образом
let data = [1,1,3,5,5]; // This is our array of numbers
// The mean is the sum of the elements divided by the number of elements
let total = 0;
for(let i = 0; i < data.length; i++) total += data[i];
let mean = total/data.length; // mean == 3; The mean of our data is 3
// To compute the standard deviation, we first sum the squares of
// the deviation of each element from the mean.
total = 0;
for(let i = 0; i < data.length; i++) {
let deviation = data[i] - mean;
total += deviation * deviation;
}
let stddev = Math.sqrt(total/(data.length-1)); // stddev == 2
Мы можем выполнить те же вычисления в кратком функциональном стиле, используя методы массива map() и reduce() следующим образом
// First, define two simple functions
const sum = (x,y) => x+y;
const square = x => x*x;
// Then use those functions with Array methods to compute mean and stddev
let data = [1,1,3,5,5];
let mean = data.reduce(sum)/data.length; // mean == 3
let deviations = data.map(x => x-mean);
let stddev = Math.sqrt(deviations.map(square).reduce(sum)/(data.length-1));
stddev // => 2
Эта новая версия кода выглядит совсем иначе, чем первая, но она все еще вызывает методы на объектах, поэтому в ней сохранились некоторые объектно-ориентированные соглашения. Давайте напишем функциональные версии методов map() и reduce()
const map = function(a, ...args) { return a.map(...args); };
const reduce = function(a, ...args) { return a.reduce(...args); };
С определенными функциями map() и reduce() наш код для вычисления среднего и стандартного отклонения теперь выглядит следующим образом
const sum = (x,y) => x+y;
const square = x => x*x;
let data = [1,1,3,5,5];
let mean = reduce(data, sum)/data.length;
let deviations = map(data, x => x-mean);
let stddev = Math.sqrt(reduce(map(deviations, square), sum)/(data.length-1));
stddev // => 2
Функции высшего порядка
Функция высшего порядка — это функция, которая работает над функциями, принимая в качестве аргументов одну или несколько функций и возвращая новую функцию.
// This higher-order function returns a new function that passes its
// arguments to f and returns the logical negation of f's return value;
function not(f) {
return function(...args) { // Return a new function
let result = f.apply(this, args); // that calls f
return !result; // and negates its result.
};
}
const even = x => x % 2 === 0; // A function to determine if a number is even
const odd = not(even); // A new function that does the opposite
[1,1,3,5,5].every(odd) // => true: every element of the array is odd
Функция not() является функцией высшего порядка, поскольку принимает аргумент функции и возвращает новую функцию. В качестве другого примера рассмотрим следующую функцию mapper(). Она принимает аргумент функции и возвращает новую функцию, которая отображает один массив в другой с помощью этой функции. Эта функция использует функцию map(), определенную ранее, и важно, чтобы вы понимали, чем отличаются эти две функции.
// Return a function that expects an array argument and applies f to
// each element, returning the array of return values.
// Contrast this with the map() function from earlier.
function mapper(f) {
return a => map(a, f);
}
const increment = x => x+1;
const incrementAll = mapper(increment);
incrementAll([1,2,3]) // => [2,3,4]
Вот еще один, более общий пример, который берет две функции, f и g, и возвращает новую функцию, которая вычисляет f(g()).
// Return a new function that computes f(g(...)).
// The returned function h passes all of its arguments to g, then passes
// the return value of g to f, then returns the return value of f.
// Both f and g are invoked with the same this value as h was invoked with.
function compose(f, g) {
return function(...args) {
// We use call for f because we're passing a single value and
// apply for g because we're passing an array of values.
return f.call(this, g.apply(this, args));
};
}
const sum = (x,y) => x+y;
const square = x => x*x;
compose(square, sum)(2,3) // => 25; the square of the sum
Частичное применение функций
Метод bind() функции f возвращает новую функцию, которая вызывает f в определенном контексте и с определенным набором аргументов. Мы говорим, что он связывает функцию с объектом и частично применяет аргументы. Метод bind() частично применяет аргументы слева — то есть аргументы, которые вы передаете в bind(), помещаются в начало списка аргументов, который передается исходной функции.
// The arguments to this function are passed on the left
function partialLeft(f, ...outerArgs) {
return function(...innerArgs) { // Return this function
let args = [...outerArgs, ...innerArgs]; // Build the argument list
return f.apply(this, args); // Then invoke f with it
};
}
// The arguments to this function are passed on the right
function partialRight(f, ...outerArgs) {
return function(...innerArgs) { // Return this function
let args = [...innerArgs, ...outerArgs]; // Build the argument list
return f.apply(this, args); // Then invoke f with it
};
}
// The arguments to this function serve as a template. Undefined values
// in the argument list are filled in with values from the inner set.
function partial(f, ...outerArgs) {
return function(...innerArgs) {
let args = [...outerArgs]; // local copy of outer args template
let innerIndex=0; // which inner arg is next
// Loop through the args, filling in undefined values from inner args
for(let i = 0; i < args.length; i++) {
if (args[i] === undefined) args[i] = innerArgs[innerIndex++];
}
// Now append any remaining inner arguments
args.push(...innerArgs.slice(innerIndex));
return f.apply(this, args);
};
}
// Here is a function with three arguments
const f = function(x,y,z) { return x * (y - z); };
// Notice how these three partial applications differ
partialLeft(f, 2)(3,4) // => -2: Bind first argument: 2 * (3 - 4)
partialRight(f, 2)(3,4) // => 6: Bind last argument: 3 * (4 - 2)
partial(f, undefined, 2)(3,4) // => -6: Bind middle argument: 3 * (2 - 4)
Эти функции частичного применения позволяют нам легко определять интересные функции из уже определенных функций.
const increment = partialLeft(sum, 1);
const cuberoot = partialRight(Math.pow, 1/3);
cuberoot(increment(26)) // => 3
Частичное применение становится еще более интересным, когда мы комбинируем его с другими функциями более высокого порядка. Вот, например, способ определения предыдущей функции not(), только что показанный, с использованием композиции и частичного применения
const not = partialLeft(compose, x => !x);
const even = x => x % 2 === 0;
const odd = not(even);
const isNumber = not(isNaN);
odd(3) && isNumber(2) // => true
Мы также можем использовать композицию и частичное применение, чтобы повторить вычисления среднего и стандартного отклонения в экстремальном функциональном стиле
// sum() and square() functions are defined above. Here are some more:
const product = (x,y) => x*y;
const neg = partial(product, -1);
const sqrt = partial(Math.pow, undefined, .5);
const reciprocal = partial(Math.pow, undefined, neg(1));
// Now compute the mean and standard deviation.
let data = [1,1,3,5,5]; // Our data
let mean = product(reduce(data, sum), reciprocal(data.length));
let stddev = sqrt(product(reduce(map(data,
compose(square,
partial(sum, neg(mean)))),
sum),
reciprocal(sum(data.length,neg(1)))));
[mean, stddev] // => [3, 2]
Обратите внимание, что этот код для вычисления среднего и стандартного отклонения полностью состоит из вызовов функций; здесь нет никаких операторов, а количество круглых скобок стало настолько большим, что этот JavaScript начинает выглядеть как код Lisp.
Опять же, это не тот стиль, который я пропагандирую для программирования на JavaScript, но это интересное упражнение, чтобы увидеть, насколько глубоко функциональным может быть код JavaScript.
поддержите меня, чтобы получить больше материалов