...

суббота, 28 февраля 2015 г.

[Перевод] Практика функционального программирования на JavaScript с использованием Ramda

Мы в 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 с налогом


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий