company_banner

Углублённое руководство по JavaScript: генераторы. Часть 2, простой пример использования

Автор оригинала: Mateusz Podlasin
  • Перевод

Поведение генераторов, описанное в предыдущей статье, нельзя назвать сложным, но оно точно удивляет и поначалу может выглядеть непонятным. Поэтому вместо изучения новых концепций мы сейчас сделаем паузу и рассмотрим интересный пример использования генераторов.

Пусть у нас есть такая функция:

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). Нужно лишь создать обёртку, которая особым образом итерирует генератор. Таким образом обёртка добавляет генератору нужную функциональность, которая может быть любо! Генераторы обладают практически безграничными возможностями, всё дело лишь в нашем воображении.
Mail.ru Group
Строим Интернет

Комментарии 9

    +2

    Похоже на чесание левой пяткой правого уха.

      0
      А можете объяснить зачем в koa.js нужны генераторы?
        0
        Так koa2 же уже где нет генераторов, разве нет?
          +1

          Тогда ещё не было async/await, поэтому генераторы использовались для имитации "плоского асинхронного кода"

            0
            Спасибо за пояснения. Генераторов нет — и это хорошо.
          0
          Это вы на генераторах написали монаду Maybe. Но у старшеклассников есть ещё много других разных монад, и когда им становится скучно, они делают то же что и вы в статье, но в более общей форме. Например, как сделал Tom Crockett в своём burrido
            0
            мне генераторы больше в таком вот приложении понравились

            function sleep(n) {return new Promise(resolve=>setTimeout(resolve, n))}
            
            async function* count(n=0) {while (true) {await sleep(100);yield n++}}
            
            const iter = count(0);
            await Promise.all(
              Array(10).fill().map(async ()=>(await iter.next()).value)
            ); // [0, 1, ,2 ,3, 4, 5, 6, 7, 8, 9]
            

              0
              а зачем столько async/await? вот тот же код работает так же…
              function* count(n=0) {while (true) {setTimeout(undefined,100);yield n++}}
              
              const iter = count(0);
              await Promise.all(
                Array(10).fill().map(()=>(iter.next()).value)
              ); //(10) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
              

              или чего-то недопонимаю?
              0

              3-ю часть бы для полноты картины

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое