Мы в rangle.io давно увлекаемся функциональным программированием, и уже опробовали Underscore и Lodash. Но недавно мы наткнулись на библиотеку Ramda, которая на первый взгляд похожа на Underscore, но отличается в небольшой, но важной области. Ramda предлагает примерно тот же набор методов, что и Underscore, но так организовывает работу с ними, что функциональная композиция становится легче.
Разница между Ramda и Underscore – в двух ключевых местах – каррирование и композиция.
Каррирование
Каррирование – превращение функции, ожидающей несколько параметров в такую, которая при передаче ей меньшего их количества возвращает новую функцию, которая ждёт остальные параметры.
R.multiply(2, 10); // возвращает 20
Мы передали функции оба параметра.
var multiplyByTwo = R.multiply(2);
multiplyByTwo(10); // возвращает 20
Круто. Мы создали новую функцию multiplyByTwo, которая по сути – 2, встроенная в multiply(). Теперь можно передать любое значение в нашу multiplyByTwo. И возможно это потому, что в Ramda все функции поддерживают каррирование.
Процесс идёт справа налево: если вы пропускаете несколько аргументов, Ramda предполагает, что вы пропустили те, что справа. Поэтому функции, принимающие массив и функцию, обычно ожидают функцию как первый аргумент и массив как второй. А в Underscore всё наоборот:
_.map([1,2,3], _.add(1)) // 2,3,4
Против:
R.map(R.add(1), [1,2,3]); // 2,3,4
Комбинируя подход «сначала операция, затем данные» с каррированием «справа налево» позволяет нам задать то, что нам надо сделать, и вернуться к функции, которая это сделает. Затем мы можем передать этой функции нужные данные. Каррирование становится простым и практичным.
var addOneToAll = R.map(R.add(1));
addOneToAll([1,2,3]); // возвращает 2,3,4
Вот пример посложнее. Допустим, мы делаем запрос к серверу, получаем массив и извлекаем значение стоимости (cost) из каждого элемента. Используя Underscore, можно было бы сделать так:
return getItems()
.then(function(items){
return _.pluck(items, 'cost');
});
Используя Ramda можно удалить лишние операции:
return getItems()
.then(R.pluck('cost'));
Когда мы вызываем R.pluck('cost'), она возвращает функцию, которая извлекает cost из каждого элемента массива. А именно это нам и надо передать в .then(). Но для полного счастья необходимо скомбинировать каррирование с композицией.
Композиция
Функциональная композиция – это операция, принимающая функции f и g, и возвращающая функцию h такую, что h(x) = f(g(x)). У Ramda для этого есть функция compose(). Соединяя два этих понятия, мы можем строить сложную работу функций из меньших компонентов.
var getCostWithTax = R.compose(
R.multiply(1 + TAX_RATE), // подсчитаем налог
R.prop('cost') // вытащим свойство 'cost'
);
Получается функция, которая вытаскивает стоимость из объекта и умножает результат на 1.13
Стандартная функция “compose” выполняет операции справа налево. Если вам это кажется контринтуитивным, можно использовать R.pipe(), которая работает, R.compose(), только слева направо:
var getCostWithTax = R.pipe(
R.prop('cost'), // вытащим свойство 'cost'
R.multiply(1 + TAX_RATE) // подсчитаем налог
);
Функции R.compose и R.pipe могут принимать до 10 аргументов.
Underscore, конечно, тоже поддерживает каррирование и композицию, но они там редко используются, поскольку каррирование в Underscore неудобно в использовании. В Ramda легко объединять эти две техники.
Сначала мы влюбились в Ramda. Её стиль порождает расширяемый, декларативный код, который легко тестировать. Композиция выполняется естественным образом и приводит к коду, который легко понимать. Но затем…
Мы обнаружили, что вещи становятся более запутанными при использовании асинхронных функций, возвращающих обещания:
var getCostWithTaxAsync = function() {
var getCostWithTax = R.pipe(
R.prop('cost'), // вытащим свойство 'cost'
R.multiply(1 + TAX_RATE) // умножим его на 1.13
);
return getItem()
.then(getCostWithTax);
}
Конечно, это лучше, чем вообще без Ramda, но хотелось бы получить что-то вроде:
var getCostWithTaxAsync = R.pipe(
getItem, // получим элемент
R.prop('cost'), // вытащим свойство 'cost'
R.multiply(1 + TAX_RATE) // умножим на 1.13
);
Но так не получится, поскольку getItem() возвращает обещание, а функция, которую вернула R.prop(), ожидает значение.
Композиция, рассчитанная на обещание
Мы связались с разработчиками Ramda и предложили такую версию композиции, которая бы автоматом разворачивала обещания, и асинхронные функции можно было бы связывать с функциями, ожидающими значение. После долгих обсуждений мы договорились на реализации такого подхода в виде новых функций: R.pCompose() и R.pPipe() – где “p” значит “promise”.
И с R.pPipe мы сможем сделать то, что нам нужно:
var getCostWithTaxAsync = R.pPipe(
getItem, // получим обещание
R.prop('cost'), // вытащим свойство 'cost'
R.multiply(1 + TAX_RATE) // умножим на 1.13
); // возвращает обещание и cost с налогом
Автор: SLY_G