Функциональное программирование на javascript

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.

поддержите меня, чтобы получить больше материалов

Оцените статью
Procodings.ru
Добавить комментарий