Асинхронная композиция функций
compose и pipe для асинхронных функций существуют в трёх контрактах: callback (error-first), Promise (then-цепочка), async/await. Все три совместимы и могут смешиваться, но имеют разный overhead.
Callback-вариант (error-first)
const compose = (...fns) => (x, cb) => {
const fn = fns.shift();
if (!fns.length) return fn(x, cb);
fn(x, (err, res) => {
if (err) return cb(err);
compose(...fns)(res, cb);
});
};
Что если случилась ошибка, то мы сразу должны вернуть её в callback и не вызывать вторую функцию. Нужен
return, чтобы дальше управление не пошло.
Принцип: контракт callback-last error-first сохраняется при компоновке. Скомпонованная функция имеет ту же сигнатуру (x, cb) => void.
Async/await-вариант
const compose = (...fns) => async (x) => {
let result = x;
for (const fn of fns) result = await fn(result);
return result;
};
Простой for-of, await на каждой итерации. Скомпонованная функция возвращает Promise.
Promise-вариант через then
const compose = (...fns) => (x) =>
fns.reduce((acc, fn) => acc.then(fn), Promise.resolve(x));
Аккумулятор в самом начале тоже должен быть промисом, чтобы мы могли на него подписаться. Reduce заменяет рекурсию.
Какой быстрее
Promise.resolve сразу создаёт зарезолвленный промис, а async-функция откладывает свое действие на микротаск. На промисах это будет выглядеть короче и проще, чем на async/await.
- async/await — самый читаемый, но создаёт лишние промисы
- pipe через then — короче, без обёртки в async IIFE
- callback — самый быстрый, но боль с error handling
Совместимость
Оба способа композиции совместимы, у них одинаковый контракт. При помощи такого compose мы можем композировать функции из предыдущего примера.
Async-функция → promise → thenable → совместимо с await. Можно смешивать.
Параллельная композиция
const parallel = (...fns) => async (x) =>
Promise.all(fns.map(fn => fn(x)));
// Использование: один вход — массив результатов
const enrichments = await parallel(getProfile, getOrders, getStats)(userId);
Как только мы переходим к асинхронщине, нам нужно реализовать параллельную композицию и последовательную. Два вида композиции.
Pipe для последовательности действий
const main = pipe(getDataset, calcProportion, renderTable);
await main('data.csv');
Я по сути pipe-ом задаю последовательность действий. Альтернатива лесенке async/await с локальными переменными.
Тонкости
void cb(...)иногда нужен, чтобы случайныйreturn cb(...)из стрелочной функции не возвращал результат callback- callback сам по себе не делает код асинхронным — это контракт, а не механизм. Функция может звать callback и синхронно
- При смешивании error-first callback и Promise — используй
promisifyиз node:util
Простота > скорости
Лучше всего это сравнивать по простоте описания и понятности кода. Если нравится compose через рекурсию — используй через рекурсию.
Связанные темы
- Композиция функций (pipe, compose)
- Композиция функций (pipe, compose)
- Async композиция и коллекторы
- Promise
- Promisify, callbackify, asyncify
Ресурсы
- Лекция 2019: 3ZCrMlMpOrM
- Лекция 2025: EI0b66cqPm4