habrahabr

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

  • воскресенье, 1 марта 2015 г. в 02:12:32
http://habrahabr.ru/post/251729/

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