Практика функционального программирования на 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 с налогом http://habrahabr.ru/post/251729/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *