[Перевод] Углублённое руководство по JavaScript: генераторы. Часть 2, простой пример использования
Поведение генераторов, описанное в предыдущей статье, нельзя назвать сложным, но оно точно удивляет и поначалу может выглядеть непонятным. Поэтому вместо изучения новых концепций мы сейчас сделаем паузу и рассмотрим интересный пример использования генераторов.
Пусть у нас есть такая функция:
function maybeAddNumbers() {
const a = maybeGetNumberA();
const b = maybeGetNumberB();
return a + b;
}
Функции maybeGetNumberA
и maybeGetNumberB
возвращают числа, но иногда могут вернуть null
или undefined
. Об этом говорит слово «maybe» в их названиях. Если такое происходит, не нужно пытаться складывать эти значения (например, число и null
), лучше сразу остановиться и вернуть, скажем, null
. Именно null
, а не какое-нибудь непредсказуемое значение, получившиеся при сложении null
/undefined
с числом или другим null
/undefined
.
Так что нужно проверять, что числа действительно определены:
function maybeAddNumbers() {
const a = maybeGetNumberA();
const b = maybeGetNumberB();
if (a === null || a === undefined || b === null || b === undefined) {
return null;
}
return a + b;
}
Всё работает, но если a
является null
или undefined
, то нет смысла вызывать функцию maybeGetNumberB
. Мы же знаем, что в любом случае будет возвращён null
.
Перепишем функцию:
function maybeAddNumbers() {
const a = maybeGetNumberA();
if (a === null || a === undefined) {
return null;
}
const b = maybeGetNumberB();
if (b === null || b === undefined) {
return null;
}
return a + b;
}
Так. Вместо трёх простых строк кода мы быстро раздули до 10 строк (не считая пустых). И в функции теперь применяются if
, через которые нужно продраться, чтобы понять, что делает функция. А это лишь учебный пример! Представьте настоящую кодовую базу с гораздо более сложной логикой, в которой такие проверки станут ещё сложнее. Вот бы применить тут генераторы и упростить код.
Взгляните:
function* maybeAddNumbers() {
const a = yield maybeGetNumberA();
const b = yield maybeGetNumberB();
return a + b;
}
Что если бы мы могли позволить выражению yield <sоmething>
проверять, является ли <sоmething>
настоящим значением, а не null
или undefined
? Если оно окажется не числом, то мы просто остановимся и вернём null
, как и в предыдущей версии кода.
То есть можно написать код, который выглядит так, словно он работает только с настоящими, определёнными значениями. Проверять это и выполнять соответствующие действия может для вас генератор! Волшебство, верно? И это не только возможно, но ещё и легко написать!
Конечно, у самих генераторов нет такой функциональности. Они просто возвращают итераторы, и при желании вы можете вставлять обратно в генераторы какие-нибудь значения. Так что нам нужно написать обёртку, пусть это будет runMaybe
.
Вместо прямого вызова функции:
const result = maybeAddNumbers();
будем вызывать её в качестве аргумента обёртки:
const result = runMaybe(maybeAddNumbers());
Это шаблон встречается в генераторах очень часто. Сами по себе они мало что умеют, но с помощью самописных обёрток вы можете придавать генераторам нужное поведение! Именно это нам сейчас нужно.
runMaybe
— функция, принимающая один аргумент: итератор, созданный генератором:
function runMaybe(iterator) {
}
Запустим этот итератор в цикле while
. Для этого нужно вызвать итератор в первый раз и запустить проверку его свойства done
:
function runMaybe(iterator) {
let result = iterator.next();
while(!result.done) {
}
}
Внутри цикла у нас есть две возможности. Если result.value
является null
или undefined
, то нужно немедленно остановить итерацию и вернуть null
. Так и сделаем:
function runMaybe(iterator) {
let result = iterator.next();
while(!result.done) {
if (result.value === null || result.value === undefined) {
return null;
}
}
}
Здесь мы с помощью return
сразу же останавливаем итерацию и возвращаем из обёртки null
. Но если result.value
является числом, то нужно «вернуть» в генератор. Например, если в yield maybeGetNumberA()
функция maybeGetNumberA()
является числом, то нужно заменить yield maybeGetNumberA()
значением этого числа. Поясню: допустим результатом вычисления maybeGetNumberA()
будет 5, тогда мы заменим const a = yield maybeGetNumberA();
на const a = 5;
. Как видите, нам не нужно менять извлечённое значение, достаточно передать его обратно в генератор.
Мы помним, что можно заменить yield <sоmething>
каким-нибудь значением, передав его в качестве аргумента методу next
в итераторе:
function runMaybe(iterator) {
let result = iterator.next();
while(!result.done) {
if (result.value === null || result.value === undefined) {
return null;
}
// we are passing result.value back
// to the generator
result = iterator.next(result.value)
}
}
Как видите, новый результат теперь снова сохраняется в переменной result
. Это возможно потому, что мы специально объявили result
с помощью let
.
Теперь если при извлечении значения генератор обнаруживает null
/undefined
, мы просто возвращаем null
из обёртки runMaybe
.
Осталось добавить что-нибудь ещё, чтобы процесс итерации завершался без обнаружения null
/undefined
. Ведь если мы получим два числа, то нужно вернуть из обёртки их сумму!
Генератор maybeAddNumbers
завершается выражением return
. Мы понимаем, что наличие return <sоmething>
в генераторе заставляет его возвращать из вызова next
объект { value: <sоmething>, done: true }
. Когда это случается, цикл while
останавливается, потому что свойство done
получает значение true
. Но последнее возвращённое значение (в нашем конкретном случае это a + b
) всё ещё будет храниться в свойстве result.value
! И мы сможем просто вернуть его:
function runMaybe(iterator) {
let result = iterator.next();
while(!result.done) {
if (result.value === null || result.value === undefined) {
return null;
}
result = iterator.next(result.value)
}
// just return the last value
// after the iterator is done
return result.value;
}
И это всё!
Создадим функции maybeGetNumberA
и maybeGetNumberB
, и пусть они возвращают сначала настоящие числа:
const maybeGetNumberA = () => 5;
const maybeGetNumberB = () => 10;
Запустим код и журналируем результат:
function* maybeAddNumbers() {
const a = yield maybeGetNumberA();
const b = yield maybeGetNumberB();
return a + b;
}
const result = runMaybe(maybeAddNumbers());
console.log(result);
Как и ожидалось, в консоли появится число 15.
Теперь заменим одно из слагаемых на null
:
const maybeGetNumberA = () => null;
const maybeGetNumberB = () => 10;
При выполнении кода получим null
!
Однако нам важно убедиться, что функция maybeGetNumberB
не вызывается, если maybeGetNumberA
возвращает null
/undefined
. Давайте снова проверим успешность вычисления. Для этого просто добавим во вторую функцию console.log
:
const maybeGetNumberA = () => null;
const maybeGetNumberB = () => {
console.log('B');
return 10;
}
Если мы верно написали обёртку runMaybe
, то при выполнении этого кода буква B
не появится в консоли.
И действительно, при выполнении кода мы увидим просто null
. Это означает, что обёртка действительно останавливает генератор, как только обнаруживает null
/undefined
.
Код работает, как задумано: выдаёт null
при любой комбинации:
const maybeGetNumberA = () => undefined;
const maybeGetNumberB = () => 10;
const maybeGetNumberA = () => 5;
const maybeGetNumberB = () => null;
const maybeGetNumberA = () => undefined;
const maybeGetNumberB = () => null;
И так далее.
Но польза этого примера кроется не в исполнении этого конкретного кода. Она кроется в факте, что мы создали универсальную обёртку, которая может работать с любым генератором, способным извлекать значения null
/undefined
.
Напишем более сложную функцию:
function* maybeAddFiveNumbers() {
const a = yield maybeGetNumberA();
const b = yield maybeGetNumberB();
const c = yield maybeGetNumberC();
const d = yield maybeGetNumberD();
const e = yield maybeGetNumberE();
return a + b + c + d + e;
}
Можно безо всяких проблем выполнить его в нашей обёртке runMaybe
! По сути, обёртке даже не важно, что наши функции возвращают числа. Ведь мы в ней не упоминали числовой тип. Так что вы можете использовать в генераторе любые значения — числа, строки, объекты, массивы, более сложные структуры данных, — и он будет работать с нашей обёрткой!
Именно это вдохновляет разработчиков. Генераторы позволяют добавлять в код свою функциональность, которая выглядит очень обычной (конечно, не считая вызовов yield
). Нужно лишь создать обёртку, которая особым образом итерирует генератор. Таким образом обёртка добавляет генератору нужную функциональность, которая может быть любо! Генераторы обладают практически безграничными возможностями, всё дело лишь в нашем воображении.