Адаптеры asyncify, promisify, callbackify

Адаптер — паттерн преобразования контракта функции. asyncify: sync → callback. promisify: callback → Promise. callbackify: Promise → callback. Тройка демонстрирует паттерн Adapter на функциях, без классов. V8 особо оптимизирует двухуровневое каррирование.

asyncify: sync → callback

const asyncify = (fn) => (...args) => {
  const callback = args.pop();
  setTimeout(() => {
    try {
      callback(null, fn(...args));
    } catch (err) {
      callback(err);
    }
  }, 0);
};

const slowAdd = asyncify((a, b) => a + b);
slowAdd(2, 3, (err, sum) => console.log(sum)); // 5 (асинхронно)

Сначала пишу asyncify — из синхронной функции делать функцию с контрактом асинхронности, а потом из этого делаем promisify.

Зачем: вписать тяжёлую sync-функцию в event loop без блокировки, либо унифицировать контракт во всей программе.

error-first callback

try {
  const result = fn(...args);
  callback(null, result);
} catch (err) {
  callback(err);
}

В колбеках принято ошибки на первый аргумент присылать. Результат — на второй. Если успешно — первый аргумент null.

Контракт Node.js: (err, data) => {}.

promisify: callback → Promise

const promisify = (fn) => (...args) =>
  new Promise((resolve, reject) => {
    fn(...args, (err, data) => err ? reject(err) : resolve(data));
  });

const readFileAsync = promisify(fs.readFile);
const data = await readFileAsync('config.json');

Уже встроено в Node.js: util.promisify.

callbackify: Promise → callback

const callbackify = (asyncFn) => (...args) => {
  const cb = args.pop();
  asyncFn(...args).then(
    (data) => cb(null, data),
    (err) => cb(err)
  );
};

// Встроен: util.callbackify
const asyncFn = async (x) => x * 2;
const cb = util.callbackify(asyncFn);
cb(5, (err, result) => console.log(result));

Зачем: интегрировать новый async-код в legacy API, которое принимает callback.

V8 оптимизирует две стрелки подряд

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

const promisify = (fn) => (...args) => /* ... */;
//              ^^^^^^^^^^^^^^^^^^^^^^^^^^ V8 видит паттерн

Это создаёт closure-фабрику. V8 не создаёт полноценную внешнюю функцию с накладными расходами — а вторая функция кэшируется как "method of bound context".

Трёх-четырёх уровней каррирования оптимизируется хуже — компилятор теряет паттерн.

Адаптер показан на функциях

Паттерн адаптер показан на функциях, не на классах. Паттерны — не только про объекты.

Классический GoF Adapter — это класс, оборачивающий другой класс под другой интерфейс. В ФП-стиле — функция-обёртка, преобразующая контракт.

Когда что использовать

Ситуация Адаптер
Sync функция блокирует event loop asyncify (setImmediate/setTimeout)
Legacy callback API в новом async-коде promisify
Async код в legacy callback API callbackify
Promise-based код в legacy stream-API разные адаптеры под кейс

Связанные темы

Ресурсы