Введение в функциональное программирование на продвинутом уровне
Функциональное программирование (ФП) — это парадигма, которая рассматривает вычисления как выполнение математических функций и избегает изменения состояния и изменяемых данных. В JavaScript, особенно в среде Node.js, ФП становится все более популярным благодаря своей предсказуемости, тестируемости и параллелизму.
Если вы уже освоили основы ФП (чистые функции, функции высшего порядка, map, filter, reduce), пришло время перейти на следующий уровень. В этой статье мы разберем продвинутые концепции: каррирование, композицию, монады и работу с иммутабельными данными.
1. Каррирование и частичное применение
Что такое каррирование?
Каррирование — это процесс преобразования функции, принимающей несколько аргументов, в последовательность функций, каждая из которых принимает один аргумент. Это позволяет создавать специализированные функции на основе общих.
// Обычная функция
const add = (a, b, c) => a + b + c;
console.log(add(1, 2, 3)); // 6
// Каррированная версия
const curriedAdd = a => b => c => a + b + c;
console.log(curriedAdd(1)(2)(3)); // 6
// Универсальная функция каррирования
const curry = (fn) => {
const arity = fn.length;
const resolver = (...args) => {
if (args.length < arity) {
return (...nextArgs) => resolver(...args, ...nextArgs);
}
return fn(...args);
};
return resolver;
};
const multiply = (a, b, c) => a * b * c;
const curriedMultiply = curry(multiply);
console.log(curriedMultiply(2)(3)(4)); // 24
console.log(curriedMultiply(2, 3)(4)); // 24
Частичное применение
Частичное применение — это фиксация части аргументов функции, создающая новую функцию с меньшим количеством аргументов. В отличие от каррирования, частичное применение не требует строгой последовательности вызовов.
const partial = (fn, ...presetArgs) => (...laterArgs) => fn(...presetArgs, ...laterArgs);
const greet = (greeting, name) => `${greeting}, ${name}!`;
const sayHello = partial(greet, 'Hello');
const sayHi = partial(greet, 'Hi');
console.log(sayHello('Alice')); // Hello, Alice!
console.log(sayHi('Bob')); // Hi, Bob!
2. Композиция функций
Продвинутая композиция
Композиция — это процесс комбинирования нескольких функций для создания новой функции. В продвинутом ФП мы используем композицию справа налево (compose) и слева направо (pipe).
// Простая композиция (справа налево)
const compose = (...fns) => (value) => fns.reduceRight((acc, fn) => fn(acc), value);
// Композиция слева направо (pipe)
const pipe = (...fns) => (value) => fns.reduce((acc, fn) => fn(acc), value);
// Пример: обработка строки
const trim = (str) => str.trim();
const toLowerCase = (str) => str.toLowerCase();
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const removeSpaces = (str) => str.replace(/\s+/g, '-');
const slugify = compose(capitalize, toLowerCase, trim, removeSpaces);
// Эквивалент: removeSpaces(trim(toLowerCase(capitalize(" Hello World "))))
console.log(slugify(' Hello World ')); // Hello-world
Трансдьюсеры (Transducers)
Трансдьюсеры — это композиция функций-трансформаций, которые работают с любыми итерируемыми структурами данных. Они позволяют объединить несколько операций (например, map и filter) в один проход.
const transducer = (mapper, predicate) => (reducer) => (acc, value) => {
const mapped = mapper(value);
return predicate(mapped) ? reducer(acc, mapped) : acc;
};
const double = (x) => x * 2;
const isEven = (x) => x % 2 === 0;
const processNumbers = transducer(double, isEven);
const sumReducer = (acc, val) => acc + val;
const numbers = [1, 2, 3, 4, 5];
const result = numbers.reduce(processNumbers(sumReducer), 0);
console.log(result); // 12 (удвоенные четные: 4 + 8)
3. Монады и функторы
Функторы
Функтор — это контейнер, который реализует метод map, позволяющий применять функцию к содержимому контейнера. В JavaScript массивы — это классические функторы.
// Простой функтор
const Functor = (value) => ({
map: (fn) => Functor(fn(value)),
valueOf: () => value,
});
const result = Functor(5)
.map((x) => x * 2)
.map((x) => x + 3);
console.log(result.valueOf()); // 13
Монада Maybe (Option)
Монада Maybe позволяет безопасно обрабатывать значения, которые могут быть null или undefined. Она состоит из двух подтипов: Just (значение есть) и Nothing (значения нет).
class Maybe {
static just(value) {
return new Just(value);
}
static nothing() {
return new Nothing();
}
static fromNullable(value) {
return value !== null && value !== undefined
? Maybe.just(value)
: Maybe.nothing();
}
}
class Just extends Maybe {
constructor(value) {
super();
this._value = value;
}
map(fn) {
return Maybe.fromNullable(fn(this._value));
}
getOrElse(defaultValue) {
return this._value;
}
}
class Nothing extends Maybe {
map(fn) {
return this;
}
getOrElse(defaultValue) {
return defaultValue;
}
}
// Пример использования
const safeGet = (obj, path) => {
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current == null) return Maybe.nothing();
current = current[key];
}
return Maybe.fromNullable(current);
};
const user = { name: 'Alice', address: { city: 'Moscow' } };
const city = safeGet(user, 'address.city')
.map((c) => c.toUpperCase())
.getOrElse