Статьи

Практическое функциональное программирование с Ramda.js

Эта статья была рецензирована Яфи Берхану , Вилданом Софтиком , Яни Хартикайнен и Дэном Принсом . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Для меня одной из вещей, которая делает JavaScript настолько интересным, является функциональный аспект языка. С самого начала функции были первоклассными гражданами в мире JavaScript. Это позволяет писать элегантный и выразительный код, который можно легко объединить несколькими способами.

Тем не менее, просто наличие возможности выполнять некоторое функциональное программирование автоматически не приводит к функциональному программированию. Ramda.js — довольно популярная библиотека (имеющая более 4 тысяч звезд на GitHub), которую мы можем использовать, чтобы помочь нам начать работу с функциональным программированием с использованием JavaScript.

Начиная

Чтобы полностью использовать Ramda.js, мы должны привыкнуть к его преимуществам, создав небольшой проект Node.js. Мы можем просто установить его через Node Package Manager (npm).

npm install ramda

Обычно мы просто импортируем функциональность библиотеки в пространство имен R Таким образом, все вызовы методов Рамды будут иметь префикс R.

 var R = require('ramda');

Конечно, ничто не мешает нам использовать Ramda.js в коде переднего плана. В браузере нам нужно только указать правильный путь к копии библиотеки. Это может быть так же просто, как следующий фрагмент HTML.

 <script src="ramda.min.js"></script>

Ramda.js не использует никаких специфических функций DOM или Node.js. Это всего лишь языковая библиотека / расширение и основана на структурах и алгоритмах, уже представленных средой выполнения JavaScript (как стандартизировано в ECMAScript 5).

Готовы окунуться? Давайте посмотрим на некоторые способности в действии!

Концепции

Наиболее важной концепцией в функциональном программировании является концепция чистых функций . Чистая функция идемпотентна и не изменит никакого состояния. Математически это имеет смысл, поскольку такие функции, как sin(x)

Помимо наличия чистых функций, мы также хотели бы иметь функции с одним аргументом. Они самые примитивные. Функции с нулевым аргументом обычно указывают, что внешнее состояние будет изменено, поэтому оно не будет чистым. Но в таком языке, как JavaScript, у нас обычно есть функции, принимающие более одного аргумента.

Карринг

Возможность иметь функции высшего порядка (то есть функции, которые могут принимать функции в качестве входных данных и испускать функцию в качестве выходных данных) в сочетании с замыканиями (захватом локальных переменных) дает нам хороший выход: каррирование . Карринг — это процесс, в котором функция с несколькими (скажем, n Это продолжается до тех пор, пока не будут собраны все необходимые аргументы.

Допустим, мы хотим использовать помощник Ramda.js для написания оболочки с одним аргументом, которая проверяет, является ли ее аргумент is Следующий код сделает эту работу.

 string

То же самое можно сделать намного проще с карри. Поскольку function isString (test) {
return R.is(String, test);
}

var result = isString('foo'); //=> true

 R.is

Это гораздо выразительнее. Поскольку мы использовали var isString = R.is(String);
var result = isString('foo'); //=> true
При втором вызове (помните, исходный вызов функции требует двух аргументов) мы получаем результат.

Но что, если мы не начнем с помощника из Ramda.js? Давайте представим, что у нас уже есть следующая функция, определенная где-то в нашем коде:

 R.is

Это полный многочлен 2-го порядка. Он имеет четыре параметра, допускающих все возможные значения. Но обычно мы хотим изменить var quadratic = (a, b, c, x) => x * x * a + x * b + c;
quadratic(1, 0, 0, 2); //=> 4
quadratic(1, 0, 0)(2); //=> TypeError: quadratic(..) is not a function
xab
Давайте посмотрим, как мы можем преобразовать это с Ramda.js:

 c

Опять же, мы можем просто использовать оценку аргумента для псевдонима определенных подмножеств. Например, уравнение var quadratic = R.curry((a, b, c, x) => x * x * a + x * b + c);
quadratic(1, 0, 0, 2); //=> 4
quadratic(1, 0, 0)(2); //=> 4

 x - 1

В случаях, когда количество аргументов не задается параметрами нашей функции, нам нужно использовать var xOffset = quadratic(0, 1, -1);
xOffset(0); //=> -1
xOffset(1); //=> 0
curryN

В основе Ramda.js лежит карринг, но без чего-либо большего библиотека кажется менее интересной. Другой концепцией, которая важна в функциональном программировании, является неизменность.

Неизменяемые структуры

Самый простой способ предотвратить изменение состояния функций — это работать только со структурами данных, которые нельзя изменить. Для простых объектов нам требуются средства доступа только для чтения, такие, что

 var position = {
    x: 5,
    y: 9
};
position.x = 10; // works!

не будет позволено Помимо объявления свойств только для чтения, мы также можем превратить их в функции получения:

 var position = (function (x, y) {
    return {
        getX: () => { return x; },
        getY: () => { return y; }
    };
})(5, 9);
position.getX() = 10; // does not work!

Теперь это уже немного лучше, однако объект можно изменить. Это означает, что кто-то может просто добавить пользовательское определение функции getX

 position.getX = function () {
  return 10;
};

Лучший способ добиться неизменности — использовать Object.freeze Вместе с ключевым словом const

 const position = Object.freeze({ x: 5, y: 9 });

Другой пример будет включать списки. Добавление элемента в неизменяемый список требует, чтобы вы сделали копию исходного списка с новым элементом, добавленным в конец. Конечно, мы также можем использовать знания об неизменности исходного объекта для оптимизации реализации. Таким образом, мы можем заменить копию простой ссылкой. По сути, это может стать своего рода связанным списком. Мы должны знать, что стандартный массив JavaScript является изменяемым и поэтому должен быть скопирован для обеспечения корректности.

Такие методы, как append() Операция идемпотентна; если мы вызываем функцию несколько раз с одинаковыми аргументами, мы всегда получим одинаковые результаты.

 R.append('tests', ['write', 'more']); //=> ['write', 'more', 'tests']
R.append('tests', ['write', 'more']); //=> ['write', 'more', 'tests']
R.append('tests', ['write', 'more']); //=> ['write', 'more', 'tests']

Существует также метод remove Это работает следующим образом:

 R.remove('write', 'tests', ['write', 'more', 'tests']); //=> ['more']

Поскольку это имеет гибкое количество аргументов, нам нужна ранее упомянутая функция curryN Есть также набор полезных общих помощников.

Полезные методы

Наиболее важной концепцией для всех вспомогательных функций является то, что аргументы упорядочены для облегчения каррирования. Чем чаще предполагается изменить аргумент, тем меньше вероятность его размещения перед каким-либо другим аргументом.

сумма () и диапазон ()

Обычные подозреваемые, такие как сумма и диапазон, конечно, можно найти в Ramda.js:

 R.sum(R.range(1, 5)); //=> 10

Поэтому для помощника range()

 var from10ToExclusive = R.range(10);
from10ToExclusive(15); //=> [10, 11, 12, 13, 14]

Что если мы хотим обернуть это фиксированным (эксклюзивным) макс. значение? Ramda.js позволяет нам использовать специальный параметр, обозначаемый R.__

 var to14FromInclusive = R.range(R.__, 15);
to14FromInclusive(10); //=> [10, 11, 12, 13, 14]

карта()

Кроме того, Ramda.js пытается предоставить альтернативы основным функциям JavaScript, таким как Array.prototype.map Эти альтернативы поставляются с другим порядком аргументов и каррингом из коробки.

Для функции map это выглядит следующим образом:

 R.map(x => 2 * x, [1, 2, 3]); //=> [2, 4, 6]

проп ()

Другой полезной утилитой является функция prop , которая пытается получить значение указанного свойства. Если данное свойство не существует, возвращается undefined Это может быть неоднозначным, если значение действительно undefined

 R.prop('x', { x: 100 }); //=> 100
R.prop('x', { y: 50 }); //=> undefined

zipWith ()

Если ранее представленные методы не убедили вас в том, что Ramda.js может предложить что-то полезное, то следующие могут быть более интересными. На этот раз мы не будем рассматривать конкретный пример, а рассмотрим произвольно выбранные сценарии.

Допустим, у нас есть два списка, и мы хотим присоединиться к ним. Это на самом деле довольно просто, используя функцию zip Тем не менее, обычный результат (массив элементов, которые сами являются двузначными массивами) может не быть желаемым. Это где функция zipWith вступает в игру. Он использует произвольную функцию для сопоставления значений одному значению.

 var letters = ["A", "B", "C", "D", "E"];
var numbers = [1, 2, 3];
var zipper = R.zipWith((x, y) => x + y);
zipper(letters, numbers); // ["A1", "B2", "C3"]

Точно так же мы могли бы ввести скалярное произведение для векторов:

 var dot = R.pipe(R.zipWith((x, y) => x * y), R.sum);
dot([1, 2, 3], [1, 2, 3]) // 14

Мы сжимаем два массива с помощью умножения (получая [1, 4, 9]

В любом случае, работа с перечислимыми — большая тема. Неудивительно, что Ramda.js предлагает множество полезных помощников. Мы уже ввели R.map Точно так же есть помощники для уменьшения количества элементов. Либо с помощью самой общей функции filter

цепь ()

Работа с массивами сопровождается несколькими полезными вспомогательными функциями. Например, используя цепочку, мы можем легко объединять массивы. Допустим, у нас есть функция reduceprimeFactorization

 R.chain(primeFactorization, [4, 7, 21]); //=> [2, 2, 7, 3, 7]

Практический пример

Все идет нормально. Теперь главный вопрос: какие преимущества мы получаем в нашей повседневной работе, используя эти концепции, представленные Ramda.js? Давайте представим, что у нас есть следующий (уже довольно красивый) фрагмент кода.

 fetchFromServer()
  .then(JSON.parse)
  .then(function (data){ return data.posts })
  .then(function (posts){ 
    return posts.map(function (post){ return post.title }) 
  });

Как можно использовать Ramda.js, чтобы сделать его еще более читабельным? Ну, первая строка настолько хороша, насколько это возможно. Второй уже загроможден. Что нам действительно нужно, так это извлечь только свойство posts

Наконец, у нас какая-то грязная третья строка. Здесь мы пытаемся перебрать все сообщения (предоставленные аргументом). Опять же, единственная цель это извлечь конкретное свойство. Как насчет следующего решения:

 fetchFromServer()
  .then(JSON.parse)
  .then(R.prop('posts'))
  .then(R.map(R.prop('title')));

Это может быть близко к оптимальному решению в отношении читабельности благодаря функциональному программированию, предоставленному Ramda.js. Однако следует отметить, что синтаксис «жирной стрелки», введенный в ECMAScript 6, также приводит к очень краткому, читабельному коду:

 fetchFromServer()
  .then(JSON.parse)
  .then(json => json.posts)
  .then(posts => posts.map(p => p.title));

Это почти так же читабельно, не требуя знания Ramda.js. Кроме того, мы сократили количество абстракций, которые могут быть полезны только для производительности и удобства обслуживания.

линзы

Наконец, мы также должны поговорить о полезных объектных помощниках. Здесь стоит упомянуть функцию объектива .

Линза — это специальный объект, который может быть передан вместе с объектом или массивом в определенные функции Ramda.js. Это позволяет этим функциям извлекать или преобразовывать данные из определенного свойства или индекса объекта или массива соответственно.

Допустим, у нас есть объект с двумя ключами xy Вместо того, чтобы оборачивать объект в другой объект методами getter и setter, мы можем создать линзы, чтобы «сфокусироваться» на интересующих свойствах.

Чтобы создать линзу, которая получает доступ к свойству x

 var x = R.lens(R.prop('x'), R.assoc('x'));

Хотя propassoc — это функция установки (синтаксис с тремя значениями: ключ, значение, объект).

Теперь мы можем использовать функции из Ramda.js для доступа к свойствам, определенным этим объективом.

 var xCoordinate = R.view(x, position);
var newPosition = R.set(x, 7, position);

Обратите внимание, что операция не затрагивает данный объект position

Следует отметить, что set — это только специализация over , которая похожа, но принимает функцию вместо произвольного значения. Затем функция будет использоваться для преобразования значения. Например, следующий вызов умножит координату x на 3:

 var newPosition = R.over(x, R.multiply(3), position);

Ramda.js, lodash или что-то еще?

Конечно, один законный вопрос — зачем выбирать Ramda.js — почему бы нам не использовать lodash или что-то еще? Конечно, можно утверждать, что Ramda.js новее и поэтому должен быть лучше, но ничто не может быть дальше от истины. Правда в том, что Ramda.js был создан с учетом функциональных принципов — новые пути (для библиотеки JavaScript) в отношении размещения и выбора аргументов.

Например, итераторы списка в Ramda.js по умолчанию пропускают только элемент, а не список. С другой стороны, стандартом для других библиотек (например, lodash) является передача элемента и индекса функции обратного вызова. Это может показаться незначительной проблемой, но она не позволяет вам использовать удобные встроенные функции, такие как parseInt()

В конце концов, решение о том, что выбрать, может быть обусловлено конкретными требованиями или опытом и / или знаниями команды, но, безусловно, есть некоторые веские аргументы для того, чтобы уделить Ramda.js внимание, которого он заслуживает.

Дальнейшее чтение

Вывод

Функциональное программирование не должно рассматриваться как волшебная палочка. Вместо этого его следует рассматривать как естественное дополнение к нашему существующему набору инструментов, которое дает нам более высокую способность к компоновке, большую гибкость и большую отказоустойчивость / надежность. Современные библиотеки JavaScript уже пытаются использовать некоторые функциональные концепции, чтобы использовать эти преимущества. Ramda.js — это мощный инструмент для расширения собственного репертуара с помощью функциональных утилит.

Что вы думаете о функциональном программировании? Где ты видишь это сияние? Дай мне знать в комментариях!