javascript

Ramda.js — библиотека, которая избавит вас от reduce и map-каши

  • вторник, 4 марта 2025 г. в 00:00:05
https://habr.com/ru/companies/otus/articles/886512/

Привет, Хабр!

Если вас когда‑либо раздражало, что Array.prototype.map нельзя использовать для объектов или reduce постоянно требует передавать начальное значение, Ramda.js решает эти проблемы, делая код чище, декларативнее и удобнее.

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

Каррирование по умолчанию

Каррирование — это техника, при которой функция не требует все аргументы сразу, а возвращает новую функцию, ожидающую оставшиеся параметры.

Пример:

import * as R from 'ramda';

const add = R.add(10); // Создаём функцию, которая прибавляет 10
console.log(add(5)); // 15

Такой подход упрощает переиспользование кода и позволяет создавать частично применённые функции.

Композиция функций (pipe и compose)

Вместо длинных цепочек.map().filter().reduce(), где легко запутаться, в Ramda можно использовать функциональную композицию:

  • R.pipe (слева направо)

  • R.compose (справа налево)

Допустим, есть массив строк, где встречаются лишние пробелы и разный регистр:

const names = [' Alice  ', ' BOB  ', '   Eva', ' J'];

На чистом JS:

const normalizeNames = (names) =>
  names
    .map(name => name.trim())
    .map(name => name.toLowerCase())
    .filter(name => name.length > 3);

console.log(normalizeNames(names)); // ['alice', 'bob']

С Ramda:

const normalizeNames = R.pipe(
  R.map(R.trim),
  R.map(R.toLower),
  R.filter(name => name.length > 3)
);

console.log(normalizeNames(names)); // ['alice', 'bob']

Стало компактнее и читабельнее.

Фильтрация, сортировка и маппинг в API

Предположим, есть API, возвращающее массив пользователей:

[
  { "id": 1, "name": " Alice ", "age": 25, "active": true },
  { "id": 2, "name": " Bob ", "age": 30, "active": false },
  { "id": 3, "name": " Charlie ", "age": 35, "active": true }
]

Задача:

  1. Выбрать только активных пользователей.

  2. Привести их имена к нормальному виду.

  3. Вернуть массив имён.

С Lodash:

import _ from 'lodash';

const processUsers = (users) =>
  _.chain(users)
    .filter({ active: true })
    .map(user => _.trim(user.name).toLowerCase())
    .value();

С Ramda:

const processUsers = R.pipe(
  R.filter(R.propEq('active', true)),
  R.map(R.pipe(R.prop('name'), R.trim, R.toLower))
);

console.log(processUsers(users)); // ['alice', 'charlie']

Ramda избавляет от промежуточных переменных.

Работа с объектами: assoc, dissoc, evolve

В функциональном стиле нельзя мутировать объекты, но Ramda предлагает удобные методы для их изменения.

Добавление и удаление полей (assoc, dissoc)

const user = { id: 1, name: 'Alice', age: 25 };

// Добавить поле
const updatedUser = R.assoc('status', 'active', user);
console.log(updatedUser);
// { id: 1, name: 'Alice', age: 25, status: 'active' }

// Удалить поле
const withoutAge = R.dissoc('age', updatedUser);
console.log(withoutAge);
// { id: 1, name: 'Alice', status: 'active' }

Изменение вложенных структур (evolve)

const user = { name: 'Alice', stats: { points: 10, level: 2 } };

const leveledUp = R.evolve({
  stats: { points: R.add(5), level: R.inc }
}, user);

console.log(leveledUp);
// { name: 'Alice', stats: { points: 15, level: 3 } }

Это удобно при обработке сложных JSON‑структур.

Группировка данных (groupBy)

Допустим, есть массив заказов, и нужно сгруппировать их по статусу.

const orders = [
  { id: 1, status: 'pending', amount: 50 },
  { id: 2, status: 'completed', amount: 150 },
  { id: 3, status: 'pending', amount: 20 }
];

// Группировка по статусу
const groupedOrders = R.groupBy(R.prop('status'), orders);

console.log(groupedOrders);
/*
{
  pending: [
    { id: 1, status: 'pending', amount: 50 },
    { id: 3, status: 'pending', amount: 20 }
  ],
  completed: [
    { id: 2, status: 'completed', amount: 150 }
  ]
}
*/

Это полезно, например, при рендере таблиц в React.

Когда Ramda НЕ нужна?

Хотя Ramda мощна, не стоит использовать её везде. В простых случаях достаточно стандартных методов map, filter, reduce:

const users = [{ name: "Alice" }, { name: "Bob" }];
console.log(users.map(user => user.name));

Ramda полезна там, где:

  • Много операций над данными.

  • Нужно поддерживать иммутабельность.

  • Код должен быть декларативным и легко читаемым.


Используете Ramda? Делитесь комментариях!

А подробнее с Ramda можно ознакомиться здесь.

Будущим разработчикам на JS рекомендую посетить открытые уроки, которые совсем скоро проведут практикующие преподаватели Otus:

  • 5 марта: Быстрый старт в Fullstack-разработку на наглядном примере с API и JavaScript. Записаться

  • 19 марта: Прототипное наследование в JavaScript. Записаться