Почему хипстеры все сочиняют

Функциональная композиция в JavaScript

Вступление

Lodash и Underscore есть повсюду, и все же есть один суперэффективный метод, который на самом деле используют только хипстеры: сочинение.

Мы более подробно рассмотрим компоновку и попутно узнаем, почему этот метод сделает ваш код более читаемым, более удобным в обслуживании и намного более элегантным.

Основы

Мы будем использовать множество функций lodash по той простой причине, что: а) мы не хотим писать наши собственные низкоуровневые реализации, поскольку они отвлекают от того, на чем мы пытаемся сосредоточиться, и б.) Lodash широко используется и может быть легко заменен подчеркиванием или другими библиотеками или вашей собственной реализацией, если вам нужно.

Прежде чем мы погрузимся в некоторые базовые примеры, давайте вспомним, что на самом деле делает compose и как мы могли бы реализовать нашу собственную функцию compose, если это необходимо.

var compose = function(f, g) {
    return function(x) {
        return f(g(x));
    };
};

Это самая простая реализация. Присмотревшись к приведенной выше функции, вы заметите, что переданные функции на самом деле применяются справа налево. Это означает, что результат правой функции передается функции слева от нее.

Теперь присмотримся к этому коду внимательнее:

function reverseAndUpper(str) {
  var reversed = reverse(str);
  return upperCase(reversed);
}

Функция reverseAndUpper, которая сначала переворачивает данную строку, а затем прописывает ее. Мы можем переписать следующий код с помощью нашей базовой функции compose:

var reverseAndUpper = compose(upperCase, reverse);

Теперь мы можем использовать reverseAndUpper следующим образом:

reverseAndUpper('test'); // TSET

Это эквивалентно написанию:

function reverseAndUpper(str) {
  return upperCase(reverse(str));
}

только более элегантный, ремонтопригодный и многоразовый.

Способность быстро составлять функции и создавать конвейер данных может использоваться несколькими способами, обеспечивая преобразование данных по конвейеру. Представьте, что вы передаете коллекцию, отображаете ее и затем возвращаете максимальное значение в конце конвейера или превращаете заданную строку в логическое значение. Compose позволяет нам легко соединять несколько функций вместе для создания более сложных функций.

Давайте реализуем более гибкую функцию компоновки, которая может обрабатывать любое количество функций, а также любое количество аргументов. Предыдущая команда compose работает только с двумя функциями и принимает только первый переданный аргумент. Мы можем переписать функцию compose следующим образом:

var compose = function() {
  var funcs = Array.protoype.slice.call(arguments);

  return funcs.reduce(function(f,g) {
    return function() {
      return f(g.apply(this, arguments));
    };
  });
};

Эта композиция позволяет написать что-то вроде этого:

Var doSometing = compose(upperCase, reverse, doSomethingInitial); 
doSomething('foo', 'bar');

Существует большое количество библиотек, которые реализуют compose за нас. Наша функция compose должна только помогать в понимании того, что на самом деле происходит внутри функции compose с подчеркиванием или оценкой, и, очевидно, конкретные реализации различаются между библиотеками. В основном они по-прежнему делают то же самое, делая свои соответствующие функции compose взаимозаменяемыми. Теперь, когда у нас есть представление о том, что такое compose, давайте воспользуемся функцией lodash _.compose для следующего набора примеров.

Примеры

Давайте начнем с очень простого примера:

function notEmpty(str) {
    return ! _.isEmpty(str);
}

Функция notEmpty просто отменяет результат _.isEmpty.

Мы можем добиться того же, используя _.compose в lodash и написав функцию not:

function not(x) { return !x; }

var notEmpty = _.compose(not, _.isEmpty);

Теперь мы можем вызвать notEmpty для любого заданного аргумента;

notEmpty('foo'); // true
notEmpty(''); // false
notEmpty(); // false
notEmpty(null); // false

Этот первый пример очень простой, следующий более сложный:

findMaxForCollection возвращает максимальный идентификатор для данной коллекции объектов, состоящей из свойств id и value.

function findMaxForCollection(data) {
    var items = _.pluck(data, 'val');
    return Math.max.apply(null, items);
}

var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}];

findMaxForCollection(data);

Приведенный выше пример можно переписать с помощью функции compose:

var findMaxForCollection = _.compose(function(xs) { 
  return Math.max.apply(null, xs); 
}, _.pluck);

var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}];

findMaxForCollection(data, 'val'); // 6

Здесь можно многое реорганизовать.

_.pluck ожидает, что коллекция будет первой, а имя свойства (спасибо @jurstv) - вторым аргументом. Что, если бы мы могли частично применить _.pluck? В этом случае мы можем использовать каррирование, чтобы перевернуть аргументы.

function pluck(key) {
    return function(collection) {
        return _.pluck(collection, key);
    }
}

Наш findMaxForCollection все еще нуждается в доработке, мы могли бы создать нашу собственную функцию max:

function max(xs) {
    return Math.max.apply(null, xs);
}

Это позволяет нам переписать функцию compose на что-нибудь более элегантное:

var findMaxForCollection = _.compose(max, pluck('val'));

findMaxForCollection(data);

Написав нашу собственную функцию pluck, мы можем частично применить pluck с помощью «val». Теперь вы, очевидно, можете возразить, зачем писать собственный метод pluck, если lodash уже имеет эту удобную функцию _.pluck. Проблема здесь в том, что _.pluck ожидает, что коллекция будет первым аргументом, а это то, чего мы не хотим. Переворачивая аргументы, мы смогли сначала частично применить ключ, нам нужно было только вызвать данные для возвращаемой функции.

Мы еще можем переписать метод выщипывания. Lodash поставляется с удобным методом _.curry, который позволяет нам написать нашу функцию pluck следующим образом:

function plucked(key, collection) {
    return _.pluck(collection, key);
}

var pluck = _.curry(plucked);

Мы просто хотим обернуть исходную функцию извлечения, чтобы мы могли перевернуть аргументы. Теперь pluck просто продолжает возвращать функцию до тех пор, пока не будут предоставлены все аргументы. Посмотрим на окончательный код:

function max(xs) {
    return Math.max.apply(null, xs);
}

function plucked(key, collection) {
    return _.pluck(collection, key);
}

var pluck = _.curry(plucked);

var findMaxForCollection = _.compose(max, pluck('val'));

var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}];

findMaxForCollection(data); // 6

findMaxForCollection можно легко прочитать справа налево, что означает, что для каждого элемента в коллекции возвращается свойство val, а затем получается максимальное значение для всех заданных значений.

var findMaxForCollection = _.compose(max, pluck('val'));

Это приводит к более удобному в сопровождении, многократному использованию и особенно элегантному коду. Мы рассмотрим последний пример, чтобы подчеркнуть элегантность функциональной композиции.

Давайте расширим данные из предыдущего примера и добавим еще одно свойство с именем active. Теперь данные следующие:

var data = [{id: 1, val: 5, active: true}, 
            {id: 2, val: 6, active: false }, 
            {id: 3, val: 2, active: true }];

У нас есть функция под названием getMaxIdForActiveItems (data), которая ожидает коллекцию объектов, фильтрует все активные элементы и возвращает максимальный идентификатор из этих отфильтрованных элементов.

function getMaxIdForActiveItems(data) {
    var filtered = _.filter(data, function(item) {
        return item.active === true;
    });

    var items = _.pluck(filtered, 'val');
    return Math.max.apply(null, items);
}

Что, если бы мы могли преобразовать приведенный выше код во что-то более элегантное? У нас уже есть функции max и pluck, поэтому все, что нам нужно сделать, это добавить фильтрацию:

var getMaxIdForActiveItems = _.compose(max, pluck('val'), _.filter);

getMaxIdForActiveItems(data, function(item) {
   return item.active === true; 
}); // 5

_.filter имеет ту же проблему, что и _.pluck, то есть мы не можем частично применить его, поскольку первый аргумент - это коллекция. Мы можем перевернуть аргументы фильтра, обернув встроенную реализацию фильтра:

function filter(fn) {
    return function(arr) {
        return arr.filter(fn);
    };
}

Еще одно усовершенствование - добавить функцию isActive, которая просто ожидает элемент и проверяет, установлен ли активный флаг в значение true.

function isActive(item) {
    return item.active === true;
}

Мы можем частично применить filter с помощью isActive, что позволяет нам вызывать getMaxIdForActiveItems только с данными.

var getMaxIdForActiveItems = 
_.compose(max, pluck('val'), filter(isActive));

Теперь все, что нам нужно передать, это данные:

getMaxIdForActiveItems(data); // 5

Это также позволяет нам легко составить функцию, которая возвращает максимальный идентификатор для любых неактивных элементов:

var isNotActive = _.compose(not, isActive);

var getMaxIdForNonActiveItems = _.compose(max, pluck('val'), filter(isNotActive));

Округлять

Функциональная композиция может быть очень интересной, как видно из предыдущих примеров, плюс приводит к более элегантному и многократно используемому коду, если применяется правильно.

Ссылки

Lodash

Эй, Underscore, ты делаешь это неправильно!

Первоначально опубликовано на busypeoples.github.io.