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

http://developer.telerik.com/featured/practical-functional-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 с налогом
Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 34
    +1
    Помню в прошлом году, когда я только знакомился с Ramda, вот эта статья очень сильно вдохновила (сорри, на англ.языке).
      0
      Функции R.compose и R.pipe могут принимать до 10 аргументов.
      Непонятны ни причина ограничения, ни истина этого высказывания. В коде ограничения нету.
        0
        Подтверждаю. Интересно, откуда у автора возникла такая строка? А переводчик не проверил.

        Возможно, в старых версиях было такое ограничение.

        Я сразу полез проверить, может там применена страшная оптимизация, с развёрткой кода в «китайский стиль», но нет, всё чисто.
          0
          Могу только предположить, что подобное ограничение могло бы быть связано с классами, где применить Function::apply не получится.
          Например:
          function fn(){}
          function call(){
           return fn.apply(null, arguments);
          }
          
          function class(){}
          function make(a,b,c,d,e,f,g,h,i,j,k,l){
           // apply вместе с new не применишь, только так
           return new class(a,b,c,d,e,f,g,h,i,j,k,l);
          }
          
            +2
            Вполне возможно. Такое неприятное ограничение на конструктор имеет место быть.

            Правда, существует и для этой неприятности обходной манёвр:

            function make () {
              var instance = Object.create(class.prototype);
              class.apply(instance, arguments);
              return instance;
            }
            
        +3
        Композиция, рассчитанная на обещание
        А почему бы не использовать композицию с помощью .then? Может, я не вижу ошибки, но разве следуюший код не выглядит ещё лучше и более знакомым программистам, нежели pPipe?
        var getCostWithTaxAsync = getItem()
          .then(R.prop('cost'))
          .then(R.multiply(1 + TAX_RATE);
        

          0
          При этом они сами приводят пример такой композиции выше:

          return getItems()
              .then(R.pluck('cost'));
          
          0
          После Ruby меня в JS убивает один нюанс: нет выбора между функциями, которые модифицируют объект (массив, строку...), и теми, которые возвращают новый экземпляр.

          Большинство востребованных мной функций — возвращают новый экземпляр. Например, .map(), .reverse(), .filter(), .toLowerCase(). А иногда очень нужно изменить сам объект, потому что им пользуются другие части приложения.

          С обратным направлением проще, так как всегда можно деструктивную функцию предварить .slice(), что возвращает новый экземпляр. Но писать .slice().sort(...), .slice().splice(...) тоже напрягает.

          Почему ни одна библиотека вроде LoDash и Ramda не предлагает функции парами, типа .map() и .mapForce()?

          PS Ramda понравилась наличием .mapObj(). Во всех остальных библиотеках приходилось вместо этого изгаляться с .reduce().
            0
            Думаю лучше так:

            list.filter( odd ) // мутируем существующий объект
            
            var list2 = list.filter( odd , [ 1 , 2 ] ) // мутируем переданный вторым аргументом
            
              0
              Вы так JavaScript сломаете.
                +1
                Он сломан by design :-)
                  +1
                  Я имею в виду, что переопределив `Array.prototype.filter` вы потеряете возможность запускать любой JS-код, кроме вашего собственного, рассчитанного на иное поведение `filter`.
                    0
                    var list = $my.list( 1 , 2 , 3 )
                    
                      0
                      Ок, так лучше, но тогда будет путаница между API JS и API Ramda.
                        0
                        Он же обратно совместим с js-апи (если не использовать в нём бесполезный второй параметр).
              +2
              В основу функционального программирования положены чистые функции. Всякие mapForce и mapObj уже из другой оперы.
                –3
                Функциональное программирование малость оторвано от жизни.
                  +3
                  Равно как и любое другое.
                    –4
                    Не равно. Компьютер оперирует битами и их состояниями. Человек мыслит объектами и их состояниями. ФП — это как эсперанто для общения между русским и украинцем.
                      +1
                      Вот именно. Компьютер оперирует битами, поэтому все эти строки, классы, шаблоны малость оторваны от жизни. Только машкоды, только хардкор!
                        0
                        Очень смешно, но вы всё же подумайте над моим высказыванием. Там немного больше смысла, чем вы заметили при беглом прочтении.
                          0
                          Вы знаете, я за свою жизнь столько наслушался тезисов от противников (или даже несторонников) ФП, что оно нинужно — т. к. компьютер императивен/мышление императивно/вселенная императивна (нужное подчеркнуть), — что вряд ли найду в вашем высказывании что-то, на что пришлось бы придумывать новый контраргумент, или, тем паче, поставило бы в тупик.
                            0
                            Я как раз сторонник ФП, но без фанатизма. Язык программирования служит для объяснения машине что от неё хочет человек. Языки программирования развивались от понятных машине, но не понятных человеку, до понятных человеку и зубодробительных для машины. Какой смысл использовать идиомы чуждые как человеку так и машине в равной степени?
                              0
                              А вы уверены, что прям-таки совсем чуждо? И что, допустим, чуждость мышлению — имманентная, принципиальная, а не приобретённая из-за того, что 99% начинуют учиться программированию с императивных языков?
                                0
                                Мы мыслим так, как взаимодействуем с миром. А взаимодействуем мы с ним объектно ориентированно.
                                  0
                                  Очень интересно. Когда я пишу ручкой на бумаге, кто есть объект, и как мы взаимодействуем? «я.взять(ручка).написать(текст, бумага)»? Или всё-таки «бумага.написать(текст, ручка, я)»? Или даже «текст.появиться(бумага, я.взять(ручка))»? А в ситуациях, когда нет явно выраженного агенса и пациенса, то есть предполагаемые объекты равноправны?
                                  Думаю, вы и без меня знаете, сколько копий сломано на тему того, что считать объектом, насколько на самом деле адекватно ООП отражает т. н. реальный мир, и как со всем этим жить.
                                  0
                                  Нам легче воспринимать иерархии, но если говорить про реальный мир, то здесь скорей есть объекты наделённые свойствами. Например есть свойства «ходить», «видеть сны», «мыслить», ими мы наделяем объекты. А это уже привет в сторону классов типов.
                                    0
                                    Важно то, что свойства этих объектов могут меняться.
                    0
                    Оторвано или нет, но ООП тоже не решение всех проблем. А шаблоны проектирования, это не способ сделать ваш код прекрасным, это костыль который помогает частично решить эти проблемы.
                0
                Вот еще библиотека для ФП nullobject.github.io/fkit/api.html
                А еще, не могу не посоветовать это замечательный туториал github.com/timoxley/functional-javascript-workshop/
                  0
                  После выхода lodash-fp теперь даже не очень понятна судьба Рамды. И неясно будет ли доводить Далтон lodash-fp до ума. Пока что к сожалению приходится использовать все три вместе: lodash, lodash-fp и ramdajs, каждая из этих библиотек имеет свои особенности и к сожалению пока ни одна их них не способна полностью заменить другую.
                    0
                    Я не нашел в документации R.pPipe и R.pCompose. На данный момент есть R.pipeP и R.composeP.
                      0
                      Что думаете о Functional.js?

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое