[Перевод] Практика функционального программирования на 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 с налогом