[Из песочницы] Ленивые функции в JavaScript
Привет!
Подумал я тут рассказать вам о том, как в JavaScript с помощью библиотеки Fluture можно создавать и использовать ленивые функции. Это будет краткий обзор на то, как создавать функции, как обрабатывать ошибки и чуть-чуть про параллелизм. Функциональным программированием мозги парить не буду! Обещаю!
Fluture
Fluture — библиотека, разработанная разработчиком Aldwin Vlasblom, реализующая Future. Future — альтернатива Promise, имеющая куда более мощный API, позволяющий реализовать отмену выполнения (cancellation), безопасную «рекурсию», «безошибочное» выполнение (используя Either) и ещё маленькую тележку крутых возможностей.
Думаю, стоит рассказать вам про методы (монады), которые я буду использовать в примерах ниже
.of(Any)
— создает Future из переданного значения.map(Function)
— нет, это неArray.map
, это функция трансформации, аналогичнаяPromise.then
.chainRej(Function)
— аналогичноPromise.catch
ловит ошибку.fork(Function, Function)
— запускает выполнение Future
Создание ленивой функции
Для себя я выделил два основных подхода к созданию ленивых функций в Fluture. Первый подход заключается в том, что мы создаем функцию, которая принимает исходные данные и возвращает готовую к выполнению Future. Второй подход заключается в том, что мы создаем Future со всеми описанными трансформациями, а затем передаем ей данные.
Непонятно? Давайте на примере! Есть у нас вот такая функция
const multiply10 = x => x * 10;
Теперь сделаем её ленивой, используя первый подход
const multiply10 = x => x * 10;
const lazyMultiply10 = (x) =>
Future
.of(x) // Создаем Future из значения
.map(multiply10); // Теперь наша функция тут
lazyMultiply10(2).fork(console.error, console.log);
// -> 20
Слишком громоздко, не правда ли? Попробуем записать более лаконично, используя второй подход.
const multiply10 = x => x * 10;
const lazyMultiply10 = Future.map(multiply10);
const value = Future.of(2); // Оборачиваем наше значение в Future
lazyMultiply10(value).fork(console.error, console.log);
// -> 20
Уже лучше, но все еще громоздко. Надо компактнее!
const lazyMultiply10 = Future.map(x => x * 10);
lazyMultiply10(Future.of(2)).fork(console.error, console.log);
// -> 20
На самом деле, эти подходы не являются взаимоисключающими и могут быть использоваться вместе.
const lazyMultiply10 = Future.map(x => x * 10);
const someCalculation = a =>
Future
.of(a)
.map(v => v + 1)
.chain(v => lazyMultiply10(Future.of(v));
someCalculation(10).fork(console.error, console.log);
// -> 110
Обработка ошибок
Обработка ошибок в Future практически не отличается от обработки ошибов в Promise. Давайте вспомним представим функцию, которая делает запрос к стороннему, не очень стабильному, API.
const requestToUnstableAPI = query =>
request({
method: 'get',
uri: `http://unstable-site.com/?${query}`
})
.then(res => res.data.value)
.catch(errorHandler);
Та же функция, но обернутая в Future
const lazyRequestToUnstableAPI = query =>
Future
.tryP(() => request({
method: 'get',
uri: `http://unstable-site.com/?${query}`
}))
.map(v => v.data.value)
.chainRej(err => Future.of(errorHandler(err));
На самом деле, обработку ошибок можно сделать более гибкой. Для этого нам понадобится структура Either, а это малость выходит за рамки моего обзора.
Параллелизм
Для работы с параллелизмом в Future реализованы два метода race(Futures[])
(аналогичен Promise.race
), parallel(n, Futures[])
и both(Future, Future)
, но он является частным случаем parallel
.
Метод parallel
принимает два аргумента, количество параллельно выполняемых Future и массив с Future. Чтобы сделать поведение parallel
таким же как метод Promise.all
, нужно количество выполняемых установить как Infinity
.
Тут тоже без примеров не обойдемся
const requestF = o => Future.tryP(() => request(o));
const parallel1 = Future.parallel(1);
const lazyReqs = parallel1(
[
'http://site.com',
'http://another-site.com',
'http://one-more-site.com',
]
.map(requestF)
);
lazyReqs.fork(console.error, console.log);
// -> [Result1, Result2, Result3]
Совместимость с Promise
В JavaScript от Promise никуда не деться, да и вряд ли кто-то будет рад, если ваш метод будет возвращать какую-то непонятную Future. Для этого у Future есть метод .promise()
, который, запустит выполнение Future и обернет её в Promise.
Future
.of(10)
.promise();
// -> Promise{value=10}
Ссылки
Вот, пожалуй, и все, что я вам хотел рассказать. Если тема интересна, дайте знать, расскажу подробнее про обработку ошибок. И да, сильно меня не ругайте, это мой первый пост на Хабре.