Skip to content

Latest commit

 

History

History
2254 lines (1606 loc) · 121 KB

File metadata and controls

2254 lines (1606 loc) · 121 KB

You Don't Know JS: Async & Performance

Глава 4: генераторы

В главе 2 мы выделили два ключевых недостатка организации асинхронности с помощью колбэков:

  • Организация асинхронности, построенная на колбэках, не соответствует той модели, по которой мы привыкли планировать действия.
  • Колбэки не слишком надёжны и их трудно комбинировать из-за инверсии контроля.

В Главе 3 мы подробно описали, как промисы помогают нам справиться с инверсией контроля, создаваемой колбэками, возвращая нам надёжность/компонуемость.

А теперь мы обратим наше внимание на способ организации контроля над асинхронным потоком в последовательной, синхронной манере. "Магия", которая делает возможным такой способ написания кода, называется ES6 генераторы.

Разрушаем систему "выполнение-до-завершения"

В Главе 1 мы говорили, что разработчики JS привыкли ожидать о своего кода - если функция была вызвана, то она выполняется до конца, и никакой другой код не может прервать этот процесс и вклиниться посередине.

Как бы странно это ни звучало, ES6 предоставляет нам новый тип функций, с отличным от привычного нам способа поведения "выполнения-до-завершения". Этот новый тип функций называется "генератор".

Чтобы немного разобраться, давайте посмотрим на этот пример:

var x = 1;

function foo() {
	x++;
	bar();				// <-- что насчет вот этой строки?
	console.log( "x:", x );
}

function bar() {
	x++;
}

foo();					// x: 3

В этом примере мы точно знаем, что bar() выполнится между x++ и console.log(x). Но что, если бы bar() тут не было? В таком случае результат был бы 2 вместо 3.

А теперь давайте пораскинем мозгами. Что если бы bar() здесь не было, но она всё ещё каким-то образом выполнялась бы между x++ и console.log(x)? Как это было бы возможно?

В языках с вытесняющей многопоточностью функция bar() вполне могла бы прервать выполнение foo() и выполниться прямо между этими двумя инструкциями. Но JS не является ни вытесняющим, ни многопоточным (в настоящее время) языком. Впрочем, кооперативная форма «прерывания» (параллелизма) вполне возможна, если foo() самостоятельно каким-либо образом сигнализирует о приостановке процесса выполнения кода.

Важно: Я использовал слово "кооперативный" не только потому, что это напрямую связано с классической терминологией параллелизма (смотрите Главу 1), но и потому, что, как вы увидите в примере ниже, синтаксис ES6 сигнализирует о приостановке процесса выполнения с помощью ключевого слова yield — предлагая вежливую «кооперативную» передачу контроля.

А вот пример того, как ES6 код достигает кооперативного параллелизма:

var x = 1;

function *foo() {
	x++;
	yield; // пауза!
	console.log( "x:", x );
}

function bar() {
	x++;
}

Важно: Скорее всего, в документациях и других примерах кода вы чаще будете видеть генераторы, объявленные вот таким способом function* foo() { .. } вместо того способа, который использовал я function *foo() { .. } - однако единственная разница здесь лишь в расположении *. Обе формы абсолютно идентичны, и функционально, и синтаксически, так же как и форма без использования пробелов function*foo() { .. }. Существуют аргументы в пользу и того, и другого стиля, но я обычно предпочитаю function *foo.. - так можно довольно удобно ссылаться на то, мы используем именно генератор, указывая в тексте просто *foo(). Если же мы используем просто foo() не всегда очевидно, о чём именно идёт речь. Но это чисто стилистические предпочтения.

Итак, как мы может выполнить код из предыдущего примера таким образом, чтобы bar() вызвалась в точке, обозначенной ключевым словом yield внутри *foo()?

// создадим итератор `it`, чтобы контролировать работу генератора
var it = foo();

// вызываем foo()`
it.next();
x;						// 2
bar();
x;						// 3
it.next();				// x: 3

Хорошо, в этих двух фрагментах кода есть немного нового и потенциально непонятного для нас материала, так что давайте разбираться. Однако прежде чем мы приступим к объяснению механики и разбору тонкостей синтаксиса, связанных с ES6 генераторами, давайте посмотрим, что здесь происходит:

  1. Эта операция it = foo() ещё не запускает выполнение генератора *foo(), здесь только создается итератор, который будет контролировать исполнение. Дальше мы ещё подробнее поговорим об итераторах.

  2. Следующая строка it.next() запускает генератор, здесь выполняется x++ на первой строке внутри *foo().

  3. Затем *foo() приостанавливает работу на ключевом слове yield, это означает, что первый вызов it.next() завершен. В этот момент *foo() всё ещё выполняется, однако находится в состоянии паузы.

  4. Мы получаем значение x, сейчас оно равно 2.

  5. Мы вызываем функцию bar(), она увеличивает значение x ещё на единицу (x++).

  6. Снова проверяем значение x, теперь оно равняется 3.

  7. Последний вызов it.next() говорит о том, что генератор *foo() вышел из состояния паузы, выполняется console.log(..), и он выводит текущее значение x, которое равно3.

Очевидно, что *foo() запустилась, но не выполнилась сразу до конца - она приостановила свою работу на yield. Затем мы возобновили выполнение *foo() и дали ей выполниться до конца, однако это не было обязательным условием.

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

Ввод и вывод

Генератор - это особый тип функции, имеющий совершенно новую модель выполнения, о которой мы только что говорили. Однако это всё еще функция, и основные принципы её работы не изменились — генератор тоже принимает аргументы (aka «ввод») и возвращает значение (aka «вывод»):

function *foo(x,y) {
	return x * y;
}

var it = foo( 6, 7 );

var res = it.next();

res.value;		// 42

Мы передали аргументы 6 и 7 в *foo(..) в качестве параметров x и y соответственно. И *foo(..) возвращает вызывающему коду значение 42

И вот здесь мы можем увидеть разницу между тем, как ведут себя в процессе вызова генератор и обычная функция. foo(6,7) выглядит знакомо, однако, в отличие от обычной функции, генератор в этот момент еще не начал выполняться.

Вместо этого, здесь мы создаём объект итератор, который присваиваем переменной it — он будет контролировать выполнение *foo(..). Затем мы вызываем it.next(), тем самым давая понять генератору, что надо продолжить выполнение из его текущего местоположения - до следующего yield или до самого конца.

Результатом вызова next(..) является объект со свойством value, содержащим значение, которое возвращает *foo(..). Другими словами, yield вычисляет и возвращает значение в процессе выполнения генератора, создавая что-то типа "промежуточного return".

Опять же, пока не очевидно, зачем нам нужен какой-то косвенный объект итератора для управления генератором. Однако мы обсудим и это - запомните это обещание (В оригинале игра слов — Promise с английского переводится как «Обещание». Примечание переводчика.).

Обмен сообщениями во время итераций

В дополнение к тому, что генераторы могут принимать аргументы и возвращать значения, они имеют даже еще более мощную встроенную систему ввода и вывода сообщений, реализованную через yield и next(..).

Рассмотрим пример:

function *foo(x) {
	var y = x * (yield);
	return y;
}

var it = foo( 6 );

// запускаем `foo(..)`
it.next();

var res = it.next( 7 );

res.value;		// 42

Сначала мы передаем 6 в качестве параметра x. Затем мы вызываем it.next(), это запускает выполнение *foo(..).

Внутри *foo(..) инструкция var y = x .. начинает выполняться, и затем она доходит до ключевого слова yield. Здесь *foo(..) приостанавливает свою работу (прямо во время операции присвоения!) и запрашивает значение у исполняющего кода, которое должно быть передано в yield. Затем мы вызываем it.next( 7 ), тем самым пробрасывая 7 как результат приостановленного выражения yield.

Поэтому в этой точке мы получаем выражение var y = 6 * 7. Return y возвращает нам 42 - и это значение становится результатом вызова it.next( 7 ).

Обратите внимание на один момент - он очень важен, хоть и может поначалу сбивать с толку даже уже опытных разработчиков - существует несоответствие между количествами yield и вызовами next(..). Как правило, вам приходится делать на один next(..) больше, чем у вас есть операторов yield - в предыдущем примере кода использовался один yield и два вызова next(..).

Из-за чего появляется такое несоответствие?

Первый вызов next(..) запускает генератор, и он доходит до первого yield . Второй же вызов уже выполняет приостановившуюся на первом yield функцию, третий выполняет второй yieldи так далее.

История о двух вопросах

Как вы думаете, какой код, в первую очередь повлияет на результат, есть ли тут предполагаемое несоответствие или нет.

Рассмотрим код самого генератора:

var y = x * (yield);
return y;

Этот первый yield как бы задаёт вопрос: "Какое значение здесь должно быть?"

И кто же отвечает на это вопрос? Первый вызов next() привёл генератор к этой точке, поэтому он не может дать ответ. Поэтому второй вызов next(..) отвечает на вопрос, заданный первым yield.

Видите несоответствие — второй-первому?

А теперь давайте посмотрим с другой стороны. Взглянем на процесс с точки зрения генератора как итератора.

Чтобы как можно лучше это проиллюстрировать, нам необходимо понять, что сообщения могут двигаться в обоих направлениях — yield .. как выражение может посылать сообщения в ответ на вызов next(..), и next(..) может передавать значения в приостановленный yield. Давайте посмотрим на следующий немного изменённый код:

function *foo(x) {
	var y = x * (yield "Hello");	// <-- возвращаем значение!
	return y;
}

var it = foo( 6 );

var res = it.next();	// первый вызов `next()`, не передаём ничего
res.value;				// "Hello"

res = it.next( 7 );		// передаём `7` в`yield`
res.value;				// 42

yield .. и next(..) работают вместе, имея возможность обмениваться сообщениями в двустороннем порядке во время выполнения генератора.

Теперь давайте посмотрим на код итератора:

var res = it.next();	// первый вызов `next()`, ничего не передаём
res.value;				// "Hello"

res = it.next( 7 );		// передаём `7` в ожидающий значения `yield`
res.value;				// 42

Важно: Мы не передаём никакого значения в первый вызов next(), и на это есть причины. Только поставленный на паузу yield может принимать значение, передаваемое вызовом next(..), а в начале работы генератора, когда мы вызываем первый next(), генератор ещё не содержит в себе "замороженного" yield, который мог бы принять значение. Спецификации и все совместимые браузеры просто молча отбрасывают (игнорируют) всё, что будет передано в первый вызов next(). Поэтому передавать туда значение является не слишком хорошей идеей, так как в этом случае вы создаёте код, который может просто молча "упасть" без каких-либо ошибок, и это может сбивать с толку. По этой причине мы всегда запускаем первый вызов next(), не передавая в него аргументы.

Первый next() (в который ничего не передано) как бы задаёт вопрос: "Какое следующее значение должен передать мне генератор*foo(..)?" И кто же отвечает на этот вопрос? Первое выражение yield "hello".

Теперь видите? Нет никакого несоответствия.

В зависимости от того кто, на ваш взгляд, задаёт вопрос, здесь может существовать или не существовать несоответствие между yield и next(..).

Но подождите! У нас всё ещё есть на один вызов next() больше, если сравнивать с количеством инструкций yield. Так, последний вызов it.next(7) снова задаёт вопрос о следующем значении, которое создаёт генератор. Но инструкции yield, которая должна была бы дать ответ, больше не осталось, и что тогда? Кто ответит на этот запрос?

И здесь инструкция return приходит нам на помощь!

Если в генераторе нет прямой инструкции return — в генераторах нет необходимости в return, в отличие от обычных функций — здесь есть скрытый/неявный return; (aka return undefined;), который и возвращает значение на поставленный финальным вызовом it.next(7) вопрос.

Запросы и ответы — двусторонняя система общения и взаимодействия между yield и next(..) — это очень мощный механизм, но всё ещё не очень понятно, как эта система связана с организацией асинхронности в нашем коде. Поговорим как раз об этом!

Множественные итераторы

Из-за особенностей синтаксиса может показаться, что когда вы создаёте итератор для управления работой генератора, вы управляете функцией самостоятельно. Это тот нюанс, который очень легко упустить из виду: каждый раз, когда вы создаёте итератор, вы неявно создаёте экземпляр генератора, который этот итератор будет контролировать.

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

function *foo() {
	var x = yield 2;
	z++;
	var y = yield (x * z);
	console.log( x, y, z );
}

var z = 1;

var it1 = foo();
var it2 = foo();

var val1 = it1.next().value;			// 2 <-- yield 2
var val2 = it2.next().value;			// 2 <-- yield 2

val1 = it1.next( val2 * 10 ).value;		// 40  <-- x:20,  z:2
val2 = it2.next( val1 * 5 ).value;		// 600 <-- x:200, z:3

it1.next( val2 / 2 );					// y:300
										// 20 300 3
it2.next( val1 / 4 );					// y:10
										// 200 10 3

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

Давайте посмотрим, что здесь происходит:

  1. Оба экземпляра *foo() начинают работать в одно и то же время, и оба вызова next() в поле value возвращают значение 2, взятое из инструкции yield 2 соответственно.
  2. val2 * 10 становится 2 * 10, и это значение передаётся в первый экземпляр генератора it1, поэтому x получает значение 20. z увеличивается (инкрементируется) с 1 на 2, и затем 20 * 2 возвращается наружу через yield, присваивая val1 значение 40.
  3. val1 * 5 становится 40 * 5, и это значение присваивается второму экземпляру генератора it2, поэтому x получает значение 200. z снова увеличивается на 1, с 2 до 3, и затем 200 * 3 снова возвращается через yield, устанавливая val2 значение 600.
  4. val2 / 2 это 600 / 2, это передаётся в первый экземпляр генератора it1, поэтому y получает значение 300, и затем выводятся значения 20 300 3 для x y z соответственно.
  5. val1 / 4 это 40 / 4, это значение попадает во второй экземпляр генератора it2, y получает значение 10, в консоль выводится значение 200 10 3 для значений x y z.

Это «забавный» пример, который можно выполнить в уме. Вы смогли это сделать?

Чередование

Вернёмся к сценарию "Выполнение-до-завершения", описанному в Главе 1:

var a = 1;
var b = 2;

function foo() {
	a++;
	b = b * a;
	a = b + 3;
}

function bar() {
	b--;
	a = 8 + b;
	b = a * 2;
}

В контексте обычных JavaScript-функциях либо foo(), либо bar() выполнятся до самого конца, однако невозможно чередовать выполнение инструкций foo() и bar(). Таким образом, в предыдущей программе есть только два возможных результата.

Однако, при использовании генераторов вполне возможно реализовать чередование выполнения инструкций (даже посреди выражения!):

var a = 1;
var b = 2;

function *foo() {
	a++;
	yield;
	b = b * a;
	a = (yield b) + 3;
}

function *bar() {
	b--;
	yield;
	a = (yield 8) + b;
	b = a * (yield 2);
}

В зависимости от того, в каком порядке вызываются итераторы, управляющие * foo () и * bar (), результатом выполнения программы могут становится разные значения. Другими словами, чередуя итерации генератора, выполняемые над одними и теми же значениями, фактически мы можем проиллюстрировать (немного условно) теоретические условия состояния гонки ("threaded race conditions"), о которых мы говорили в Главе 1.

Для начала, давайте создадим вспомогательную функцию step(..), которая будет контролировать iterator:

function step(gen) {
	var it = gen();
	var last;

	return function() {
		// какое бы значение ни было возвращал yield,
		// оно будет передано в следующий вызов next()!
		last = it.next( last ).value;
	};
}

step(..) инициализирует генератор, создавая итератор it, он, в свою очередь, возвращает функцию, вызов которой продвигает итератор на шаг вперёд. Кроме того, предыдущее значение, которое возвращает yield, предаётся в следующий вызов next. Поэтому yield 8 становится значением 8 и yield b становится b (то есть, именно тем значением, которое было определено рядом с yield).

А теперь давайте поэкспериментируем - чтобы получше познакомиться с эффектом чередования различных кусочков *foo() и *bar(). Начнём с довольно простого базового примера, убедимся, что *foo() полностью выполнилась прежде, чем начала выполняться *bar() (точно так же, как в примере в Главе 1):

// убедимся, что `a` и `b` получили исходные значения
a = 1;
b = 2;

var s1 = step( foo );
var s2 = step( bar );

// `*foo()` полностью выполнилась до самого конца
s1();
s1();
s1();

// теперь выполняется `*bar()`
s2();
s2();
s2();
s2();

console.log( a, b );	// 11 22

В конце мы получаем результаты 11 и 22, точно так же, как и в версии этого примера в Главе 1. Теперь давайте смешаем порядок чередования инструкций генераторов и посмотрим, как это изменит окончательные значения a и b:

// снова убедимся, что `a` и `b` получили исходные значения
a = 1;
b = 2;

var s1 = step( foo );
var s2 = step( bar );

s2();		// b--;
s2();		// yield 8
s1();		// a++;
s2();		// a = 8 + b; // 9
			// yield 2
s1();		// b = b * a;
			// yield b
s1();		// a = b + 3;
s2();		// b = a * 2;

Сможете догадаться, каким значениям будут равны a и b после выполнения программы, прежде чем я скажу вас результаты? Только не подглядывайте!

console.log( a, b );	// 12 18

На заметку: Чтобы потренироваться, попробуйте еще поизменять порядок вызовов s1() и s2() и посмотрите, какие ещё комбинации ответов вы получите. Однако не забывайте, что вам всегда нужно сделать три вызова s1() и четыре вызова s2(). А для того, чтобы понять, почему именно так, обратитесь снова к тому моменту, где мы обсуждали соотношение next() и yield.

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

Мы обсудим параллельность в контексте генераторов более детально в конце главы.

Генерирование значений

В предыдущей части мы рассмотрели такой интересный способ использования генераторов, как продуцирование значений. Это не является основной темой этой главы, однако с нашей стороны было бы серьёзным упущением не пройтись подробно по ключевым концепциям. В особенности, не остановиться на той, чей механизм лёг в основу самого названия - «генераторы».

Сейчас мы с вами немного отойдём от темы и уклонимся в стороны изучения итераторов, а затем вернёмся назад, чтобы понять, как они связаны с генераторами и использованием генераторов для генерирования значений.

Продьюсеры и итераторы

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

Вы можете создать что-то подобное, используя замыкания (подробнее почитать об этом вы можете в части Замыкания и Области видимости серии YDKJS):

var gimmeSomething = (function(){
	var nextVal;

	return function(){
		if (nextVal === undefined) {
			nextVal = 1;
		}
		else {
			nextVal = (3 * nextVal) + 6;
		}

		return nextVal;
	};
})();

gimmeSomething();		// 1
gimmeSomething();		// 9
gimmeSomething();		// 33
gimmeSomething();		// 105

Важно: Мы могли бы значительно упростить вычислительную логику nextVal, однако мы намеренно не хотим вычислять следующее значение (aka nextVal) до того момента, пока не будет сделан следующий вызов gimmeSomething(), в противном случае это был бы довольно ресурсоёмкий способ реализации продьюсера для значений, более сложных, чем просто числа.

Генерирование произвольной серии чисел не кажется достаточно реалистичным примером. Но что, если вам необходимо генерировать записи из какого-то источника данных? Вы могли бы представить себе примерно такой же код.

На самом деле, это довольно распространённый шаблон проектирования, более известный как итератор. Итератор - это чётко определённый интерфейс для перебора серии значений с помощью некого продьюсера. Как и в большинстве других языков, интерфейс итератора в JavaScript организован с помощью вызова метода next() каждый раз, когда мы хотим получить следующее значение.

Мы можем реализовать стандартный интерфейс итератора для нашего продьюсера серии чисел:

var something = (function(){
	var nextVal;

	return {
		// необходимо для использования `for..of` циклов
		[Symbol.iterator]: function(){ return this; },

		// стандартный метод интерфейса итератора
		next: function(){
			if (nextVal === undefined) {
				nextVal = 1;
			}
			else {
				nextVal = (3 * nextVal) + 6;
			}

			return { done:false, value:nextVal };
		}
	};
})();

something.next().value;		// 1
something.next().value;		// 9
something.next().value;		// 33
something.next().value;		// 105

Важно: Мы ещё затронем тему, зачем нам нужна вот эта часть в нашем примере [Symbol.iterator]: .. в секции про "Итерируемость". Если говорить о синтаксисе, то здесь используется сразу две новые фичи ES6. Первая - это вычисляемые имена свойств, для которых используется вот такой синтаксис [ .. ] (читайте об этом больше в части this & Прототипы). Это способ определить выражение внутри литералов объекта и использовать результат его вычисления в качестве имени свойства. Вторая, Symbol.iterator- это один из встроенных ES6 символов (Symbol) (смотрите часть ES6 & Beyond серии YDKJS).

Вызов метода next() возвращает объект с двумя свойствами: done - это поле в качестве значения содержит boolean и отражает завершено ли выполнение итератора; второе поле, value, содержит собственно значение итератора.

ES6 также добавляет сюда цикл for..of, что означает, что итератор может автоматически выполняться с помощью встроенного синтаксиса цикла:

for (var v of something) {
	console.log( v );

	// не давайте циклу выполняться бесконечно!
	if (v > 500) {
		break;
	}
}
// 1 9 33 105 321 969

Важно: Поскольку наш итератор something всегда возвращает done:false, цикл for..of будет выполняться бесконечно, поэтому мы поместили внутрь условия инструкцию break. Вообще для итераторов это совершенно нормально - выполняться бесконечно, но существуют ситуации, когда итератору необходимо пройтись по некоторому конечному числу (набору) значений и затем вернуть done:true.

Цикл for..of автоматически вызывает next() во время каждой итерации — и он не передаёт никаких значений вnext() — и автоматически прекратит своё выполнение, как только итератор возвратит done:true. Таким образом довольно удобно перебирать некий набор данных.

Конечно, мы можем управлять итератором вручную, вызывая next() самостоятельно и проверяя значение поля done, чтобы знать, где прекратить выполнение итератора:

for (
	var ret;
	(ret = something.next()) && !ret.done;
) {
	console.log( ret.value );

	// не позволяйте циклу выполняться бесконечно!
	if (ret.value > 500) {
		break;
	}
}
// 1 9 33 105 321 969

Важно: Такой ручной подход к управлению с использованием for выглядит определённо менее изящным, чем ES6-синтаксис цикла for..of, но он предоставляет нам возможность передать значение в вызов next(..), если это необходимо.

В дополнение к возможности создания собственной реализации итераторов, многие встроенные в JS (а также и в ES6) структуры данных, такие как массивы, также по умолчанию содержат в себе итераторы:

var a = [1,3,5,7,9];

for (var v of a) {
	console.log( v );
}
// 1 3 5 7 9

Цикл for..of вызывает iterator a и автоматически использует его, чтобы перебирать значения в массиве a.

Важно: это может показаться довольно странным упущением стандарта ES6, но обычные object намеренно не имеют в себе по умолчанию итератор, как массивы. И причины этого скрыты намного глубже, чем мы могли бы объяснить здесь. Если вы всё же хотели бы иметь возможность итерироваться по свойствам объекта (однако соблюдение порядка прохода по этим свойствам не гарантировано), Object.keys(..) возвращает структуру типа array, по которой можно пройти с помощью for (var k of Object.keys(obj)) { ... Такой способ использования цикла for..of для прохода по ключам объекта очень похож на принцип действия циклаfor..in, за исключением того, что Object.keys(..) не имеет доступа к свойствам, лежащим в цепочке [[Prototype]], в то время как for..in имеет этот функционал (подробнее вы можете почитать об этом в части this & Прототипы).

Итерируемый (Iterables)

Объект something из нашего примера можно назвать реализацией итератора, так как он имеет метод next() в своём интерфейсе. Но ближе к нему будет термин итерируемый, что означает, что something включает в себя итератор, коротый может проходить по значениям.

Начиная со стандарта ES6 появился способ получить итератор из итерируемого объекта или массива. Для этого итерируемый должен иметь функцию, названием которой является значение ES6-символа Symbol.iterator. При вызове такой функции возвращается итератор. Хотя это не является обязательным, обычно каждый вызов возвращает новый итератор.

В предыдущем примере кода, a - это пример такого итерируемого массива. Цикл for..of автоматически вызывает функцию Symbol.iterator для того, чтобы создать итератор. Конечно же, мы имеем возможность вызывать функцию вручную, используя итератор, который она возвращает:

var a = [1,3,5,7,9];

var it = a[Symbol.iterator]();

it.next().value;	// 1
it.next().value;	// 3
it.next().value;	// 5
..

В предыдущем примере кода, когда вы определяли something, могли заметить, одну интересную строчку кода:

[Symbol.iterator]: function(){ return this; }

Этот немного запутанный код возвращает значение something. Интерфейс итератора something также является итерируемым, теперь он одновременно и итерируемый, и итератор. Затем мы помещаем (передаём) something в цикл for..of:

for (var v of something) {
	..
}

Цикл for..ofожидает, что something будет итерируемым, поэтому он ищет и вызывает функцию Symbol.iterator. Мы определяем в теле этой функции простое return this, поэтому она просто возвращает саму себя, однако сам по себе цикл for..of не настолько умён.

Генератор Итератор

Теперь давайте вернёмся к генераторам и посмотрим на них в контексте наших новых знаний об итераторах. Генератор может рассматриваться как продьюсер значений, которые мы получаем последовательно с помощью вызова метода next(), являющегося частью интерфейса итератора.

Технически генератор не является итерируемым, хотя ведёт себя очень похоже - когда вы вызываете генератор, в ответ вы получаете итератор:

function *foo(){ .. }

var it = foo();

Мы можем реализовать something из нашего старого примера с генератором как продьюсер бесконечного числа значений, таким образом:

function *something() {
	var nextVal;

	while (true) {
		if (nextVal === undefined) {
			nextVal = 1;
		}
		else {
			nextVal = (3 * nextVal) + 6;
		}

		yield nextVal;
	}
}

Важно: Обычно использование цикла while..true в реальном JS-коде является плохой практикой, особенно, если не предполается break или return - в таком случае цикл будет выполняться бесконечно, синхронно и полностью заблокирует весь поток выполнения. Однако для генератора использование такого цикла является совершенно нормальным явлением благодаря оператору yield. С помощью него генератор будет приостанавливаться на каждой итерации, возвращаясь обратно в основной поток выполнения и/или в очередь цикла событий (event loop). Безусловно, "генераторы вернули while..true обратно в JS программирование!"

Это уже выглядит намного более чисто и понятно, верно? Благодаря тому, что генератор останавливает свою работу на каждом yield, состояние (область видимости) функции *something() сохраняется, и нам не нужно использовать замыкания, для того чтобы сохранять значения переменных между вызовами функции.

Получается не только более простой - нет необходимости создавать собственный итератор - но и более понятный код, потому что довольно легко предположить, как он себя ведёт. Например, цикл while..trueподсказывает нам, что генератор будет выполняться бесконечно - генерируя значения до тех пор, пока мы их запрашиваем.

И теперь мы можем написать совершенно новый генератор *something(), используя цикл for..of, и мы увидим, что это работает очень схоже:

for (var v of something()) {
	console.log( v );

	// не позволяйте циклу выполняться бесконечно!
	if (v > 500) {
		break;
	}
}
// 1 9 33 105 321 969

Но присмотритесь внимательнее к for (var v of something()) ..! Мы не просто указали something в качестве значения, как мы делали в предыдущих примерах, вместо этого мы вызываем генератор *something(), чтобы получить новый итератор для использования в цикле for..of.

Если вы были достаточно внимательны, у вас могло возникнуть два вопроса касательно взаимодейтсвия генератора и цикла:

  • Почему мы не могли просто написать for (var v of something) ..? Всё потому, что something здесь - это генератор, и он не является итерируемым. Нам необходимо вызвать something() - таким образом мы создадим продьюсер, по которому сможем итерироваться с помощью цикла for..of.
  • Вызов something() возвращает итератор, но ведь для цикла for..of необходим итерируемый, верно? Верно. Возвращаемый генератором итератор также содержит в себе функцию Symbol.iterator, которая просто возвращает саму себя, точно так же, итерируемый something, который мы определили до этого. Другими словами, возвращаемый генератором итератор и сам является итерируемым!

Остановка работы генератора

В предыдущем примере экземпляр итератора, созданный генератором *something(), так и остался бы навсегда в приостановленном состоянии после того, как мы бы добрались до инструкции break в цикле.

Однако существует скрытый механизм, который позаботится об этом для вас. "Аномальное завершение" (или "раннее прекращение") - оно может быть вызвано break, return или необработанной ошибкой - посылает итератору сигнал о том, что необходимо прекратить работу.

Важно: Технически, цикл for..of посылает такой же сигнал итератору и при обычном завершении цикла. Для генератора это, по сути, неактуальная операция, так как итератор генератора должен завершиться до того, как полностью выполнится цикл for..of. Однако некоторые итераторы могут захотеть получить этот дополнительный сигнал завершения из цикла for..of.

Тогда как цикл for..of автоматически отправляет сигнал, при желании вы можете самостоятельно послать сигнал итератору - для этого вам нужно вызвать return(..).

Если вы используете внутри генератора конструкцию try..finally, она выполнится даже после завершения работы генератора. Это может быть очень полезно в случае, если вам нужно освободить ресурсы (соединение с базой данных и т.д.).

function *something() {
	try {
		var nextVal;

		while (true) {
			if (nextVal === undefined) {
				nextVal = 1;
			}
			else {
				nextVal = (3 * nextVal) + 6;
			}

			yield nextVal;
		}
	}
	// cleanup clause
	finally {
		console.log( "cleaning up!" );
	}
}

В примере, который мы видели ранее, break в цикле for..of вызывал finally. Однако вы можете вручную прекратить работу экземпляра итератора, использовав метод return(..) снаружи:

var it = something();
for (var v of it) {
	console.log( v );

	// не позволяйте циклу выполняться бесконечно!
	if (v > 500) {
		console.log(
			// завершает выполнение итератора
			it.return( "Hello World" ).value
		);
		// нет необходимости использовать `break`
	}
}
// 1 9 33 105 321 969
// cleaning up!
// Hello World

Вызов it.return(..) мгновенно прекращает выполнение генератора, однако конструкция finally выполняется. Кроме того, он устанавливает возвращаемое значение - то, что вы передадите в return(..), таким образом мы получаем "Hello World" обратно. Нам нет необходимости использовать инструкцию break, так как итератор, возвращаемый генератором, уже получил done:true и на следующей итерации цикла for..of прекратит своё выполнение.

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

И теперь, когда мы имеем более полное понимание того, как работают генераторы, мы можем перейти к тому, как генераторы связаны с процессом управления асинхронным потоком.

Асинхронное итерирование

Как генераторы могут справляться с асинхронным потоком, решать проблемы с колбэками и подобные? Давайте ответим на этот важный вопрос.

Давайте снова вернёмся к одному из сценариев с использованием колбэков из Главы 3:

function foo(x,y,cb) {
	ajax(
		"http:https://some.url.1/?x=" + x + "&y=" + y,
		cb
	);
}

foo( 11, 31, function(err,text) {
	if (err) {
		console.error( err );
	}
	else {
		console.log( text );
	}
} );

Если мы хотим воспроизвести похожий сценарий с помощью генераторов, мы можем написать:

function foo(x,y) {
	ajax(
		"http:https://some.url.1/?x=" + x + "&y=" + y,
		function(err,data){
			if (err) {
				// возвращаем ошибку в `*main()`
				it.throw( err );
			}
			else {
				// возвращаемся в `*main()` с полученными данными
				it.next( data );
			}
		}
	);
}

function *main() {
	try {
		var text = yield foo( 11, 31 );
		console.log( text );
	}
	catch (err) {
		console.error( err );
	}
}

var it = main();

// запускаем выполнение!
it.next();

На первый взгляд эта реализация выглядит более объёмной и ,возможно, более сложной, чем вариант с колбэками, который мы видели прежде. Однако не поддавайтесь первому впечатлению! Вариант с генераторами намного более эффективный! И сейчас мы узнаем, почему.

Давайте внимательно рассмотрим первую часть кода, она является наиболее важной:

var text = yield foo( 11, 31 );
console.log( text );

Подумайте о том, как именно работает этот код. Мы вызываем обычную функцию foo(..), однако мы можем получить значение text, возвращаемое нам из Ajax-запроса, несмотря на то, что он является асинхронным.

Как это возможно? Давайте вернёмся в Главу 1, где мы уже видели очень похожий код:

var data = ajax( "..url 1.." );
console.log( data );

Однако этот код не работает! В чем же разница? Вся разница заключается в использовании yield внутри генератора.

Похоже на магию! Это позволяет нам писать блокирующий, синхронный код, однако, он не будет блокировать выполнение всей программы - он будет блокировать (ставить на паузу) только выполнение кода внутри самого генератора.

В yield foo(11,31), во время первого вызова foo(11,31), нам ничего не возвращается (aka undefined), мы делаем запрос данных, однако мы всё еще получаем yield undefined. Это нормально, потому что код в настоящее время не полагается на значение yield для выполнения чего-либо интересного. Чуть позже мы еще вернёмся к этому фрагменту.

Здесь мы используем yield не для того, чтобы обмениваться сообщениями, а для того, чтобы контролировать поток выполнения. Мы используем возможность передачи сообщений, однако только в одном направлении, после того, как будет возобновлена работа генератора.

Поэтому, когда генератор приостанавливает свою работу на yield, как бы задаёт вопрос: «Какое значение здесь я должен вернуть и присвоить переменной text?». Кто должен ответить на этот вопрос?

Посмотрим на функцию foo(..). Если Ajax-запрос выполнился успешно, мы вызовем:

it.next( data );

Мы возобновляем работу генератора с полученными данными, что означает, что мы передаём значение прямо в приостановивший работу генератора yield, и затем мы снова запускаем выполнение функции-генератора с новым, присвоив значение локальной переменной text.

Неплохо, неправда ли?

Давайте сделаем шаг назад и посмотрим ещё раз. У нас получился абсолютно синхронный на первый взгляд код (кроме самого значения yield), однако, скрытно, внутри функции foo(..), выполнялись асинхронные операции.

Это же потрясающе! Мы нашли практически идеальное решение для проблем с колбэками, которые мы рассматривали прежде - каким образом мы можем управлять асинхронным потом в последовательной, синхронной манере, которой соответствует наш образ мышления.

По сути, мы вынесли асинхронный поток как часть реализации, для того, чтобы мы могли в синхронной/последовательной манере рассуждать об управлении потоком программы: "Сделай Ajax-запрос и по окончанию выведи результат". Здесь у нас появилось лишь два шага в процессе управления асинхронным потоком, однако, в такой манере мы не имеем ограничений и можем использовать столько шагов, сколько нам необходимо.

Совет: Это настолько важный момент, что я бы рекомендовал вам вернуться и еще раз внимательно перечитать три предыдущих параграфа, чтобы как следует погрузиться в тему!

Синхронное управление обработкой ошибок

Код с генератором, который мы видели прежде, имеет еще несколько преимуществ. Давайте обратим наше внимание на конструкцию try..catch внутри генератора:

try {
	var text = yield foo( 11, 31 );
	console.log( text );
}
catch (err) {
	console.error( err );
}

Как именно это работает? Вызов foo(..) происходит асинхронно, почему же try..catch не падает с ошибкой, как мы видели в Главе 3?

Как мы уже видели, yield приостановил процесс операции присвоения, чтобы подождать, пока выполнится функция foo(..), и это позволило нам присвоить результат запроса переменной text. Помимо этого yield позволяет генератору обрабатывать ошибки самым удивительным образом. В предыдущем примере кода мы выбросили ошибку внутри функции-генератора:

if (err) {
	// выбрасываем ошибку в `*main()`
	it.throw( err );
}

Благодаря инструкции yield внутри функций-генераторов мы не только можем в синхронной манере получать значения из функций, выполняющих асинхронные запросы, но и обрабатывать ошибки в синхронной манере.

Мы видим код, связанный с обработкой ошибок внутри генератора, однако как же с обработкой ошибок вне функции-генератора? Точно так же, как мы и привыкли:

function *main() {
	var x = yield "Hello World";

	yield x.toLowerCase();	// вызовет ошибку!
}

var it = main();

it.next().value;			// Hello World

try {
	it.next( 42 );
}
catch (err) {
	console.error( err );	// TypeError
}

Конечно, мы можем и вручную выбросить ошибку, используя throw .. вместо того, чтобы сгенерировать ошибку.

Мы можем использовать catch для того, чтобы обработать ошибку внутри самого генератора, однако если этого не произойдёт, код итератора сможет справиться с этим:

function *main() {
	var x = yield "Hello World";

	// никогда не заходит сюда
	console.log( x );
}

var it = main();

it.next();

try {
	// сможет ли  `*main()` обработать ошибку?
	it.throw( "Oops" );
}
catch (err) {
	// нет, ошибка не была обработана!
	console.error( err );			// Oops
}

Обработка ошибок в синхронной манере (через try..catch) в асинхронном коде - это большой плюс в пользу уровня читаемости и предсказуемости кода.

Generators + Promises

In our previous discussion, we showed how generators can be iterated asynchronously, which is a huge step forward in sequential reason-ability over the spaghetti mess of callbacks. But we lost something very important: the trustability and composability of Promises (see Chapter 3)!

Don't worry -- we can get that back. The best of all worlds in ES6 is to combine generators (synchronous-looking async code) with Promises (trustable and composable).

But how?

Recall from Chapter 3 the Promise-based approach to our running Ajax example:

function foo(x,y) {
	return request(
		"http:https://some.url.1/?x=" + x + "&y=" + y
	);
}

foo( 11, 31 )
.then(
	function(text){
		console.log( text );
	},
	function(err){
		console.error( err );
	}
);

In our earlier generator code for the running Ajax example, foo(..) returned nothing (undefined), and our iterator control code didn't care about that yielded value.

But here the Promise-aware foo(..) returns a promise after making the Ajax call. That suggests that we could construct a promise with foo(..) and then yield it from the generator, and then the iterator control code would receive that promise.

But what should the iterator do with the promise?

It should listen for the promise to resolve (fulfillment or rejection), and then either resume the generator with the fulfillment message or throw an error into the generator with the rejection reason.

Let me repeat that, because it's so important. The natural way to get the most out of Promises and generators is to yield a Promise, and wire that Promise to control the generator's iterator.

Let's give it a try! First, we'll put the Promise-aware foo(..) together with the generator *main():

function foo(x,y) {
	return request(
		"http:https://some.url.1/?x=" + x + "&y=" + y
	);
}

function *main() {
	try {
		var text = yield foo( 11, 31 );
		console.log( text );
	}
	catch (err) {
		console.error( err );
	}
}

The most powerful revelation in this refactor is that the code inside *main() did not have to change at all! Inside the generator, whatever values are yielded out is just an opaque implementation detail, so we're not even aware it's happening, nor do we need to worry about it.

But how are we going to run *main() now? We still have some of the implementation plumbing work to do, to receive and wire up the yielded promise so that it resumes the generator upon resolution. We'll start by trying that manually:

var it = main();

var p = it.next().value;

// wait for the `p` promise to resolve
p.then(
	function(text){
		it.next( text );
	},
	function(err){
		it.throw( err );
	}
);

Actually, that wasn't so painful at all, was it?

This snippet should look very similar to what we did earlier with the manually wired generator controlled by the error-first callback. Instead of an if (err) { it.throw.., the promise already splits fulfillment (success) and rejection (failure) for us, but otherwise the iterator control is identical.

Now, we've glossed over some important details.

Most importantly, we took advantage of the fact that we knew that *main() only had one Promise-aware step in it. What if we wanted to be able to Promise-drive a generator no matter how many steps it has? We certainly don't want to manually write out the Promise chain differently for each generator! What would be much nicer is if there was a way to repeat (aka "loop" over) the iteration control, and each time a Promise comes out, wait on its resolution before continuing.

Also, what if the generator throws out an error (intentionally or accidentally) during the it.next(..) call? Should we quit, or should we catch it and send it right back in? Similarly, what if we it.throw(..) a Promise rejection into the generator, but it's not handled, and comes right back out?

Promise-Aware Generator Runner

The more you start to explore this path, the more you realize, "wow, it'd be great if there was just some utility to do it for me." And you're absolutely correct. This is such an important pattern, and you don't want to get it wrong (or exhaust yourself repeating it over and over), so your best bet is to use a utility that is specifically designed to run Promise-yielding generators in the manner we've illustrated.

Several Promise abstraction libraries provide just such a utility, including my asynquence library and its runner(..), which will be discussed in Appendix A of this book.

But for the sake of learning and illustration, let's just define our own standalone utility that we'll call run(..):

// thanks to Benjamin Gruenbaum (@benjamingr on GitHub) for
// big improvements here!
function run(gen) {
	var args = [].slice.call( arguments, 1), it;

	// initialize the generator in the current context
	it = gen.apply( this, args );

	// return a promise for the generator completing
	return Promise.resolve()
		.then( function handleNext(value){
			// run to the next yielded value
			var next = it.next( value );

			return (function handleResult(next){
				// generator has completed running?
				if (next.done) {
					return next.value;
				}
				// otherwise keep going
				else {
					return Promise.resolve( next.value )
						.then(
		r					// resume the async loop on
							// success, sending the resolved
							// value back into the generator
							handleNext,

							// if `value` is a rejected
							// promise, propagate error back
							// into the generator for its own
							// error handling
							function handleErr(err) {
								return Promise.resolve(
									it.throw( err )
								)
								.then( handleResult );
							}
						);
				}
			})(next);
		} );
}

As you can see, it's a quite a bit more complex than you'd probably want to author yourself, and you especially wouldn't want to repeat this code for each generator you use. So, a utility/library helper is definitely the way to go. Nevertheless, I encourage you to spend a few minutes studying that code listing to get a better sense of how to manage the generator+Promise negotiation.

How would you use run(..) with *main() in our running Ajax example?

function *main() {
	// ..
}

run( main );

That's it! The way we wired run(..), it will automatically advance the generator you pass to it, asynchronously until completion.

Note: The run(..) we defined returns a promise which is wired to resolve once the generator is complete, or receive an uncaught exception if the generator doesn't handle it. We don't show that capability here, but we'll come back to it later in the chapter.

ES7: async and await?

The preceding pattern -- generators yielding Promises that then control the generator's iterator to advance it to completion -- is such a powerful and useful approach, it would be nicer if we could do it without the clutter of the library utility helper (aka run(..)).

There's probably good news on that front. At the time of this writing, there's early but strong support for a proposal for more syntactic addition in this realm for the post-ES6, ES7-ish timeframe. Obviously, it's too early to guarantee the details, but there's a pretty decent chance it will shake out similar to the following:

function foo(x,y) {
	return request(
		"http:https://some.url.1/?x=" + x + "&y=" + y
	);
}

async function main() {
	try {
		var text = await foo( 11, 31 );
		console.log( text );
	}
	catch (err) {
		console.error( err );
	}
}

main();

As you can see, there's no run(..) call (meaning no need for a library utility!) to invoke and drive main() -- it's just called as a normal function. Also, main() isn't declared as a generator function anymore; it's a new kind of function: async function. And finally, instead of yielding a Promise, we await for it to resolve.

The async function automatically knows what to do if you await a Promise -- it will pause the function (just like with generators) until the Promise resolves. We didn't illustrate it in this snippet, but calling an async function like main() automatically returns a promise that's resolved whenever the function finishes completely.

Tip: The async / await syntax should look very familiar to readers with experience in C#, because it's basically identical.

The proposal essentially codifies support for the pattern we've already derived, into a syntactic mechanism: combining Promises with sync-looking flow control code. That's the best of both worlds combined, to effectively address practically all of the major concerns we outlined with callbacks.

The mere fact that such a ES7-ish proposal already exists and has early support and enthusiasm is a major vote of confidence in the future importance of this async pattern.

Promise Concurrency in Generators

So far, all we've demonstrated is a single-step async flow with Promises+generators. But real-world code will often have many async steps.

If you're not careful, the sync-looking style of generators may lull you into complacency with how you structure your async concurrency, leading to suboptimal performance patterns. So we want to spend a little time exploring the options.

Imagine a scenario where you need to fetch data from two different sources, then combine those responses to make a third request, and finally print out the last response. We explored a similar scenario with Promises in Chapter 3, but let's reconsider it in the context of generators.

Your first instinct might be something like:

function *foo() {
	var r1 = yield request( "http:https://some.url.1" );
	var r2 = yield request( "http:https://some.url.2" );

	var r3 = yield request(
		"http:https://some.url.3/?v=" + r1 + "," + r2
	);

	console.log( r3 );
}

// use previously defined `run(..)` utility
run( foo );

This code will work, but in the specifics of our scenario, it's not optimal. Can you spot why?

Because the r1 and r2 requests can -- and for performance reasons, should -- run concurrently, but in this code they will run sequentially; the "http:https://some.url.2" URL isn't Ajax fetched until after the "http:https://some.url.1" request is finished. These two requests are independent, so the better performance approach would likely be to have them run at the same time.

But how exactly would you do that with a generator and yield? We know that yield is only a single pause point in the code, so you can't really do two pauses at the same time.

The most natural and effective answer is to base the async flow on Promises, specifically on their capability to manage state in a time-independent fashion (see "Future Value" in Chapter 3).

The simplest approach:

function *foo() {
	// make both requests "in parallel"
	var p1 = request( "http:https://some.url.1" );
	var p2 = request( "http:https://some.url.2" );

	// wait until both promises resolve
	var r1 = yield p1;
	var r2 = yield p2;

	var r3 = yield request(
		"http:https://some.url.3/?v=" + r1 + "," + r2
	);

	console.log( r3 );
}

// use previously defined `run(..)` utility
run( foo );

Why is this different from the previous snippet? Look at where the yield is and is not. p1 and p2 are promises for Ajax requests made concurrently (aka "in parallel"). It doesn't matter which one finishes first, because promises will hold onto their resolved state for as long as necessary.

Then we use two subsequent yield statements to wait for and retrieve the resolutions from the promises (into r1 and r2, respectively). If p1 resolves first, the yield p1 resumes first then waits on the yield p2 to resume. If p2 resolves first, it will just patiently hold onto that resolution value until asked, but the yield p1 will hold on first, until p1 resolves.

Either way, both p1 and p2 will run concurrently, and both have to finish, in either order, before the r3 = yield request.. Ajax request will be made.

If that flow control processing model sounds familiar, it's basically the same as what we identified in Chapter 3 as the "gate" pattern, enabled by the Promise.all([ .. ]) utility. So, we could also express the flow control like this:

function *foo() {
	// make both requests "in parallel," and
	// wait until both promises resolve
	var results = yield Promise.all( [
		request( "http:https://some.url.1" ),
		request( "http:https://some.url.2" )
	] );

	var r1 = results[0];
	var r2 = results[1];

	var r3 = yield request(
		"http:https://some.url.3/?v=" + r1 + "," + r2
	);

	console.log( r3 );
}

// use previously defined `run(..)` utility
run( foo );

Note: As we discussed in Chapter 3, we can even use ES6 destructuring assignment to simplify the var r1 = .. var r2 = .. assignments, with var [r1,r2] = results.

In other words, all of the concurrency capabilities of Promises are available to us in the generator+Promise approach. So in any place where you need more than sequential this-then-that async flow control steps, Promises are likely your best bet.

Promises, Hidden

As a word of stylistic caution, be careful about how much Promise logic you include inside your generators. The whole point of using generators for asynchrony in the way we've described is to create simple, sequential, sync-looking code, and to hide as much of the details of asynchrony away from that code as possible.

For example, this might be a cleaner approach:

// note: normal function, not generator
function bar(url1,url2) {
	return Promise.all( [
		request( url1 ),
		request( url2 )
	] );
}

function *foo() {
	// hide the Promise-based concurrency details
	// inside `bar(..)`
	var results = yield bar(
		"http:https://some.url.1",
		"http:https://some.url.2"
	);

	var r1 = results[0];
	var r2 = results[1];

	var r3 = yield request(
		"http:https://some.url.3/?v=" + r1 + "," + r2
	);

	console.log( r3 );
}

// use previously defined `run(..)` utility
run( foo );

Inside *foo(), it's cleaner and clearer that all we're doing is just asking bar(..) to get us some results, and we'll yield-wait on that to happen. We don't have to care that under the covers a Promise.all([ .. ]) Promise composition will be used to make that happen.

We treat asynchrony, and indeed Promises, as an implementation detail.

Hiding your Promise logic inside a function that you merely call from your generator is especially useful if you're going to do a sophisticated series flow-control. For example:

function bar() {
	return	Promise.all( [
		  baz( .. )
		  .then( .. ),
		  Promise.race( [ .. ] )
		] )
		.then( .. )
}

That kind of logic is sometimes required, and if you dump it directly inside your generator(s), you've defeated most of the reason why you would want to use generators in the first place. We should intentionally abstract such details away from our generator code so that they don't clutter up the higher level task expression.

Beyond creating code that is both functional and performant, you should also strive to make code that is as reason-able and maintainable as possible.

Note: Abstraction is not always a healthy thing for programming -- many times it can increase complexity in exchange for terseness. But in this case, I believe it's much healthier for your generator+Promise async code than the alternatives. As with all such advice, though, pay attention to your specific situations and make proper decisions for you and your team.

Generator Delegation

In the previous section, we showed calling regular functions from inside a generator, and how that remains a useful technique for abstracting away implementation details (like async Promise flow). But the main drawback of using a normal function for this task is that it has to behave by the normal function rules, which means it cannot pause itself with yield like a generator can.

It may then occur to you that you might try to call one generator from another generator, using our run(..) helper, such as:

function *foo() {
	var r2 = yield request( "http:https://some.url.2" );
	var r3 = yield request( "http:https://some.url.3/?v=" + r2 );

	return r3;
}

function *bar() {
	var r1 = yield request( "http:https://some.url.1" );

	// "delegating" to `*foo()` via `run(..)`
	var r3 = yield run( foo );

	console.log( r3 );
}

run( bar );

We run *foo() inside of *bar() by using our run(..) utility again. We take advantage here of the fact that the run(..) we defined earlier returns a promise which is resolved when its generator is run to completion (or errors out), so if we yield out to a run(..) instance the promise from another run(..) call, it automatically pauses *bar() until *foo() finishes.

But there's an even better way to integrate calling *foo() into *bar(), and it's called yield-delegation. The special syntax for yield-delegation is: yield * __ (notice the extra *). Before we see it work in our previous example, let's look at a simpler scenario:

function *foo() {
	console.log( "`*foo()` starting" );
	yield 3;
	yield 4;
	console.log( "`*foo()` finished" );
}

function *bar() {
	yield 1;
	yield 2;
	yield *foo();	// `yield`-delegation!
	yield 5;
}

var it = bar();

it.next().value;	// 1
it.next().value;	// 2
it.next().value;	// `*foo()` starting
					// 3
it.next().value;	// 4
it.next().value;	// `*foo()` finished
					// 5

Note: Similar to a note earlier in the chapter where I explained why I prefer function *foo() .. instead of function* foo() .., I also prefer -- differing from most other documentation on the topic -- to say yield *foo() instead of yield* foo(). The placement of the * is purely stylistic and up to your best judgment. But I find the consistency of styling attractive.

How does the yield *foo() delegation work?

First, calling foo() creates an iterator exactly as we've already seen. Then, yield * delegates/transfers the iterator instance control (of the present *bar() generator) over to this other *foo() iterator.

So, the first two it.next() calls are controlling *bar(), but when we make the third it.next() call, now *foo() starts up, and now we're controlling *foo() instead of *bar(). That's why it's called delegation -- *bar() delegated its iteration control to *foo().

As soon as the it iterator control exhausts the entire *foo() iterator, it automatically returns to controlling *bar().

So now back to the previous example with the three sequential Ajax requests:

function *foo() {
	var r2 = yield request( "http:https://some.url.2" );
	var r3 = yield request( "http:https://some.url.3/?v=" + r2 );

	return r3;
}

function *bar() {
	var r1 = yield request( "http:https://some.url.1" );

	// "delegating" to `*foo()` via `yield*`
	var r3 = yield *foo();

	console.log( r3 );
}

run( bar );

The only difference between this snippet and the version used earlier is the use of yield *foo() instead of the previous yield run(foo).

Note: yield * yields iteration control, not generator control; when you invoke the *foo() generator, you're now yield-delegating to its iterator. But you can actually yield-delegate to any iterable; yield *[1,2,3] would consume the default iterator for the [1,2,3] array value.

Why Delegation?

The purpose of yield-delegation is mostly code organization, and in that way is symmetrical with normal function calling.

Imagine two modules that respectively provide methods foo() and bar(), where bar() calls foo(). The reason the two are separate is generally because the proper organization of code for the program calls for them to be in separate functions. For example, there may be cases where foo() is called standalone, and other places where bar() calls foo().

For all these exact same reasons, keeping generators separate aids in program readability, maintenance, and debuggability. In that respect, yield * is a syntactic shortcut for manually iterating over the steps of *foo() while inside of *bar().

Such manual approach would be especially complex if the steps in *foo() were asynchronous, which is why you'd probably need to use that run(..) utility to do it. And as we've shown, yield *foo() eliminates the need for a sub-instance of the run(..) utility (like run(foo)).

Delegating Messages

You may wonder how this yield-delegation works not just with iterator control but with the two-way message passing. Carefully follow the flow of messages in and out, through the yield-delegation:

function *foo() {
	console.log( "inside `*foo()`:", yield "B" );

	console.log( "inside `*foo()`:", yield "C" );

	return "D";
}

function *bar() {
	console.log( "inside `*bar()`:", yield "A" );

	// `yield`-delegation!
	console.log( "inside `*bar()`:", yield *foo() );

	console.log( "inside `*bar()`:", yield "E" );

	return "F";
}

var it = bar();

console.log( "outside:", it.next().value );
// outside: A

console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B

console.log( "outside:", it.next( 2 ).value );
// inside `*foo()`: 2
// outside: C

console.log( "outside:", it.next( 3 ).value );
// inside `*foo()`: 3
// inside `*bar()`: D
// outside: E

console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: 4
// outside: F

Pay particular attention to the processing steps after the it.next(3) call:

  1. The 3 value is passed (through the yield-delegation in *bar()) into the waiting yield "C" expression inside of *foo().
  2. *foo() then calls return "D", but this value doesn't get returned all the way back to the outside it.next(3) call.
  3. Instead, the "D" value is sent as the result of the waiting yield *foo() expression inside of *bar() -- this yield-delegation expression has essentially been paused while all of *foo() was exhausted. So "D" ends up inside of *bar() for it to print out.
  4. yield "E" is called inside of *bar(), and the "E" value is yielded to the outside as the result of the it.next(3) call.

From the perspective of the external iterator (it), it doesn't appear any differently between controlling the initial generator or a delegated one.

In fact, yield-delegation doesn't even have to be directed to another generator; it can just be directed to a non-generator, general iterable. For example:

function *bar() {
	console.log( "inside `*bar()`:", yield "A" );

	// `yield`-delegation to a non-generator!
	console.log( "inside `*bar()`:", yield *[ "B", "C", "D" ] );

	console.log( "inside `*bar()`:", yield "E" );

	return "F";
}

var it = bar();

console.log( "outside:", it.next().value );
// outside: A

console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B

console.log( "outside:", it.next( 2 ).value );
// outside: C

console.log( "outside:", it.next( 3 ).value );
// outside: D

console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: undefined
// outside: E

console.log( "outside:", it.next( 5 ).value );
// inside `*bar()`: 5
// outside: F

Notice the differences in where the messages were received/reported between this example and the one previous.

Most strikingly, the default array iterator doesn't care about any messages sent in via next(..) calls, so the values 2, 3, and 4 are essentially ignored. Also, because that iterator has no explicit return value (unlike the previously used *foo()), the yield * expression gets an undefined when it finishes.

Exceptions Delegated, Too!

In the same way that yield-delegation transparently passes messages through in both directions, errors/exceptions also pass in both directions:

function *foo() {
	try {
		yield "B";
	}
	catch (err) {
		console.log( "error caught inside `*foo()`:", err );
	}

	yield "C";

	throw "D";
}

function *bar() {
	yield "A";

	try {
		yield *foo();
	}
	catch (err) {
		console.log( "error caught inside `*bar()`:", err );
	}

	yield "E";

	yield *baz();

	// note: can't get here!
	yield "G";
}

function *baz() {
	throw "F";
}

var it = bar();

console.log( "outside:", it.next().value );
// outside: A

console.log( "outside:", it.next( 1 ).value );
// outside: B

console.log( "outside:", it.throw( 2 ).value );
// error caught inside `*foo()`: 2
// outside: C

console.log( "outside:", it.next( 3 ).value );
// error caught inside `*bar()`: D
// outside: E

try {
	console.log( "outside:", it.next( 4 ).value );
}
catch (err) {
	console.log( "error caught outside:", err );
}
// error caught outside: F

Some things to note from this snippet:

  1. When we call it.throw(2), it sends the error message 2 into *bar(), which delegates that to *foo(), which then catches it and handles it gracefully. Then, the yield "C" sends "C" back out as the return value from the it.throw(2) call.
  2. The "D" value that's next thrown from inside *foo() propagates out to *bar(), which catches it and handles it gracefully. Then the yield "E" sends "E" back out as the return value from the it.next(3) call.
  3. Next, the exception thrown from *baz() isn't caught in *bar() -- though we did catch it outside -- so both *baz() and *bar() are set to a completed state. After this snippet, you would not be able to get the "G" value out with any subsequent next(..) call(s) -- they will just return undefined for value.

Delegating Asynchrony

Let's finally get back to our earlier yield-delegation example with the multiple sequential Ajax requests:

function *foo() {
	var r2 = yield request( "http:https://some.url.2" );
	var r3 = yield request( "http:https://some.url.3/?v=" + r2 );

	return r3;
}

function *bar() {
	var r1 = yield request( "http:https://some.url.1" );

	var r3 = yield *foo();

	console.log( r3 );
}

run( bar );

Instead of calling yield run(foo) inside of *bar(), we just call yield *foo().

In the previous version of this example, the Promise mechanism (controlled by run(..)) was used to transport the value from return r3 in *foo() to the local variable r3 inside *bar(). Now, that value is just returned back directly via the yield * mechanics.

Otherwise, the behavior is pretty much identical.

Delegating "Recursion"

Of course, yield-delegation can keep following as many delegation steps as you wire up. You could even use yield-delegation for async-capable generator "recursion" -- a generator yield-delegating to itself:

function *foo(val) {
	if (val > 1) {
		// generator recursion
		val = yield *foo( val - 1 );
	}

	return yield request( "http:https://some.url/?v=" + val );
}

function *bar() {
	var r1 = yield *foo( 3 );
	console.log( r1 );
}

run( bar );

Note: Our run(..) utility could have been called with run( foo, 3 ), because it supports additional parameters being passed along to the initialization of the generator. However, we used a parameter-free *bar() here to highlight the flexibility of yield *.

What processing steps follow from that code? Hang on, this is going to be quite intricate to describe in detail:

  1. run(bar) starts up the *bar() generator.
  2. foo(3) creates an iterator for *foo(..) and passes 3 as its val parameter.
  3. Because 3 > 1, foo(2) creates another iterator and passes in 2 as its val parameter.
  4. Because 2 > 1, foo(1) creates yet another iterator and passes in 1 as its val parameter.
  5. 1 > 1 is false, so we next call request(..) with the 1 value, and get a promise back for that first Ajax call.
  6. That promise is yielded out, which comes back to the *foo(2) generator instance.
  7. The yield * passes that promise back out to the *foo(3) generator instance. Another yield * passes the promise out to the *bar() generator instance. And yet again another yield * passes the promise out to the run(..) utility, which will wait on that promise (for the first Ajax request) to proceed.
  8. When the promise resolves, its fulfillment message is sent to resume *bar(), which passes through the yield * into the *foo(3) instance, which then passes through the yield * to the *foo(2) generator instance, which then passes through the yield * to the normal yield that's waiting in the *foo(3) generator instance.
  9. That first call's Ajax response is now immediately returned from the *foo(3) generator instance, which sends that value back as the result of the yield * expression in the *foo(2) instance, and assigned to its local val variable.
  10. Inside *foo(2), a second Ajax request is made with request(..), whose promise is yielded back to the *foo(1) instance, and then yield * propagates all the way out to run(..) (step 7 again). When the promise resolves, the second Ajax response propagates all the way back into the *foo(2) generator instance, and is assigned to its local val variable.
  11. Finally, the third Ajax request is made with request(..), its promise goes out to run(..), and then its resolution value comes all the way back, which is then returned so that it comes back to the waiting yield * expression in *bar().

Phew! A lot of crazy mental juggling, huh? You might want to read through that a few more times, and then go grab a snack to clear your head!

Generator Concurrency

As we discussed in both Chapter 1 and earlier in this chapter, two simultaneously running "processes" can cooperatively interleave their operations, and many times this can yield (pun intended) very powerful asynchrony expressions.

Frankly, our earlier examples of concurrency interleaving of multiple generators showed how to make it really confusing. But we hinted that there's places where this capability is quite useful.

Recall a scenario we looked at in Chapter 1, where two different simultaneous Ajax response handlers needed to coordinate with each other to make sure that the data communication was not a race condition. We slotted the responses into the res array like this:

function response(data) {
	if (data.url == "http:https://some.url.1") {
		res[0] = data;
	}
	else if (data.url == "http:https://some.url.2") {
		res[1] = data;
	}
}

But how can we use multiple generators concurrently for this scenario?

// `request(..)` is a Promise-aware Ajax utility

var res = [];

function *reqData(url) {
	res.push(
		yield request( url )
	);
}

Note: We're going to use two instances of the *reqData(..) generator here, but there's no difference to running a single instance of two different generators; both approaches are reasoned about identically. We'll see two different generators coordinating in just a bit.

Instead of having to manually sort out res[0] and res[1] assignments, we'll use coordinated ordering so that res.push(..) properly slots the values in the expected and predictable order. The expressed logic thus should feel a bit cleaner.

But how will we actually orchestrate this interaction? First, let's just do it manually, with Promises:

var it1 = reqData( "http:https://some.url.1" );
var it2 = reqData( "http:https://some.url.2" );

var p1 = it1.next().value;
var p2 = it2.next().value;

p1
.then( function(data){
	it1.next( data );
	return p2;
} )
.then( function(data){
	it2.next( data );
} );

*reqData(..)'s two instances are both started to make their Ajax requests, then paused with yield. Then we choose to resume the first instance when p1 resolves, and then p2's resolution will restart the second instance. In this way, we use Promise orchestration to ensure that res[0] will have the first response and res[1] will have the second response.

But frankly, this is awfully manual, and it doesn't really let the generators orchestrate themselves, which is where the true power can lie. Let's try it a different way:

// `request(..)` is a Promise-aware Ajax utility

var res = [];

function *reqData(url) {
	var data = yield request( url );

	// transfer control
	yield;

	res.push( data );
}

var it1 = reqData( "http:https://some.url.1" );
var it2 = reqData( "http:https://some.url.2" );

var p1 = it1.next().value;
var p2 = it2.next().value;

p1.then( function(data){
	it1.next( data );
} );

p2.then( function(data){
	it2.next( data );
} );

Promise.all( [p1,p2] )
.then( function(){
	it1.next();
	it2.next();
} );

OK, this is a bit better (though still manual!), because now the two instances of *reqData(..) run truly concurrently, and (at least for the first part) independently.

In the previous snippet, the second instance was not given its data until after the first instance was totally finished. But here, both instances receive their data as soon as their respective responses come back, and then each instance does another yield for control transfer purposes. We then choose what order to resume them in the Promise.all([ .. ]) handler.

What may not be as obvious is that this approach hints at an easier form for a reusable utility, because of the symmetry. We can do even better. Let's imagine using a utility called runAll(..):

// `request(..)` is a Promise-aware Ajax utility

var res = [];

runAll(
	function*(){
		var p1 = request( "http:https://some.url.1" );

		// transfer control
		yield;

		res.push( yield p1 );
	},
	function*(){
		var p2 = request( "http:https://some.url.2" );

		// transfer control
		yield;

		res.push( yield p2 );
	}
);

Note: We're not including a code listing for runAll(..) as it is not only long enough to bog down the text, but is an extension of the logic we've already implemented in run(..) earlier. So, as a good supplementary exercise for the reader, try your hand at evolving the code from run(..) to work like the imagined runAll(..). Also, my asynquence library provides a previously mentioned runner(..) utility with this kind of capability already built in, and will be discussed in Appendix A of this book.

Here's how the processing inside runAll(..) would operate:

  1. The first generator gets a promise for the first Ajax response from "http:https://some.url.1", then yields control back to the runAll(..) utility.
  2. The second generator runs and does the same for "http:https://some.url.2", yielding control back to the runAll(..) utility.
  3. The first generator resumes, and then yields out its promise p1. The runAll(..) utility does the same in this case as our previous run(..), in that it waits on that promise to resolve, then resumes the same generator (no control transfer!). When p1 resolves, runAll(..) resumes the first generator again with that resolution value, and then res[0] is given its value. When the first generator then finishes, that's an implicit transfer of control.
  4. The second generator resumes, yields out its promise p2, and waits for it to resolve. Once it does, runAll(..) resumes the second generator with that value, and res[1] is set.

In this running example, we use an outer variable called res to store the results of the two different Ajax responses -- that's our concurrency coordination making that possible.

But it might be quite helpful to further extend runAll(..) to provide an inner variable space for the multiple generator instances to share, such as an empty object we'll call data below. Also, it could take non-Promise values that are yielded and hand them off to the next generator.

Consider:

// `request(..)` is a Promise-aware Ajax utility

runAll(
	function*(data){
		data.res = [];

		// transfer control (and message pass)
		var url1 = yield "http:https://some.url.2";

		var p1 = request( url1 ); // "http:https://some.url.1"

		// transfer control
		yield;

		data.res.push( yield p1 );
	},
	function*(data){
		// transfer control (and message pass)
		var url2 = yield "http:https://some.url.1";

		var p2 = request( url2 ); // "http:https://some.url.2"

		// transfer control
		yield;

		data.res.push( yield p2 );
	}
);

In this formulation, the two generators are not just coordinating control transfer, but actually communicating with each other, both through data.res and the yielded messages that trade url1 and url2 values. That's incredibly powerful!

Such realization also serves as a conceptual base for a more sophisticated asynchrony technique called CSP (Communicating Sequential Processes), which we will cover in Appendix B of this book.

Thunks

So far, we've made the assumption that yielding a Promise from a generator -- and having that Promise resume the generator via a helper utility like run(..) -- was the best possible way to manage asynchrony with generators. To be clear, it is.

But we skipped over another pattern that has some mildly widespread adoption, so in the interest of completeness we'll take a brief look at it.

In general computer science, there's an old pre-JS concept called a "thunk." Without getting bogged down in the historical nature, a narrow expression of a thunk in JS is a function that -- without any parameters -- is wired to call another function.

In other words, you wrap a function definition around function call -- with any parameters it needs -- to defer the execution of that call, and that wrapping function is a thunk. When you later execute the thunk, you end up calling the original function.

For example:

function foo(x,y) {
	return x + y;
}

function fooThunk() {
	return foo( 3, 4 );
}

// later

console.log( fooThunk() );	// 7

So, a synchronous thunk is pretty straightforward. But what about an async thunk? We can essentially extend the narrow thunk definition to include it receiving a callback.

Consider:

function foo(x,y,cb) {
	setTimeout( function(){
		cb( x + y );
	}, 1000 );
}

function fooThunk(cb) {
	foo( 3, 4, cb );
}

// later

fooThunk( function(sum){
	console.log( sum );		// 7
} );

As you can see, fooThunk(..) only expects a cb(..) parameter, as it already has values 3 and 4 (for x and y, respectively) pre-specified and ready to pass to foo(..). A thunk is just waiting around patiently for the last piece it needs to do its job: the callback.

You don't want to make thunks manually, though. So, let's invent a utility that does this wrapping for us.

Consider:

function thunkify(fn) {
	var args = [].slice.call( arguments, 1 );
	return function(cb) {
		args.push( cb );
		return fn.apply( null, args );
	};
}

var fooThunk = thunkify( foo, 3, 4 );

// later

fooThunk( function(sum) {
	console.log( sum );		// 7
} );

Tip: Here we assume that the original (foo(..)) function signature expects its callback in the last position, with any other parameters coming before it. This is a pretty ubiquitous "standard" for async JS function standards. You might call it "callback-last style." If for some reason you had a need to handle "callback-first style" signatures, you would just make a utility that used args.unshift(..) instead of args.push(..).

The preceding formulation of thunkify(..) takes both the foo(..) function reference, and any parameters it needs, and returns back the thunk itself (fooThunk(..)). However, that's not the typical approach you'll find to thunks in JS.

Instead of thunkify(..) making the thunk itself, typically -- if not perplexingly -- the thunkify(..) utility would produce a function that produces thunks.

Uhhhh... yeah.

Consider:

function thunkify(fn) {
	return function() {
		var args = [].slice.call( arguments );
		return function(cb) {
			args.push( cb );
			return fn.apply( null, args );
		};
	};
}

The main difference here is the extra return function() { .. } layer. Here's how its usage differs:

var whatIsThis = thunkify( foo );

var fooThunk = whatIsThis( 3, 4 );

// later

fooThunk( function(sum) {
	console.log( sum );		// 7
} );

Obviously, the big question this snippet implies is what is whatIsThis properly called? It's not the thunk, it's the thing that will produce thunks from foo(..) calls. It's kind of like a "factory" for "thunks." There doesn't seem to be any kind of standard agreement for naming such a thing.

So, my proposal is "thunkory" ("thunk" + "factory"). So, thunkify(..) produces a thunkory, and a thunkory produces thunks. That reasoning is symmetric to my proposal for "promisory" in Chapter 3:

var fooThunkory = thunkify( foo );

var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );

// later

fooThunk1( function(sum) {
	console.log( sum );		// 7
} );

fooThunk2( function(sum) {
	console.log( sum );		// 11
} );

Note: The running foo(..) example expects a style of callback that's not "error-first style." Of course, "error-first style" is much more common. If foo(..) had some sort of legitimate error-producing expectation, we could change it to expect and use an error-first callback. None of the subsequent thunkify(..) machinery cares what style of callback is assumed. The only difference in usage would be fooThunk1(function(err,sum){...

Exposing the thunkory method -- instead of how the earlier thunkify(..) hides this intermediary step -- may seem like unnecessary complication. But in general, it's quite useful to make thunkories at the beginning of your program to wrap existing API methods, and then be able to pass around and call those thunkories when you need thunks. The two distinct steps preserve a cleaner separation of capability.

To illustrate:

// cleaner:
var fooThunkory = thunkify( foo );

var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );

// instead of:
var fooThunk1 = thunkify( foo, 3, 4 );
var fooThunk2 = thunkify( foo, 5, 6 );

Regardless of whether you like to deal with the thunkories explicitly or not, the usage of thunks fooThunk1(..) and fooThunk2(..) remains the same.

s/promise/thunk/

So what's all this thunk stuff have to do with generators?

Comparing thunks to promises generally: they're not directly interchangable as they're not equivalent in behavior. Promises are vastly more capable and trustable than bare thunks.

But in another sense, they both can be seen as a request for a value, which may be async in its answering.

Recall from Chapter 3 we defined a utility for promisifying a function, which we called Promise.wrap(..) -- we could have called it promisify(..), too! This Promise-wrapping utility doesn't produce Promises; it produces promisories that in turn produce Promises. This is completely symmetric to the thunkories and thunks presently being discussed.

To illustrate the symmetry, let's first alter the running foo(..) example from earlier to assume an "error-first style" callback:

function foo(x,y,cb) {
	setTimeout( function(){
		// assume `cb(..)` as "error-first style"
		cb( null, x + y );
	}, 1000 );
}

Now, we'll compare using thunkify(..) and promisify(..) (aka Promise.wrap(..) from Chapter 3):

// symmetrical: constructing the question asker
var fooThunkory = thunkify( foo );
var fooPromisory = promisify( foo );

// symmetrical: asking the question
var fooThunk = fooThunkory( 3, 4 );
var fooPromise = fooPromisory( 3, 4 );

// get the thunk answer
fooThunk( function(err,sum){
	if (err) {
		console.error( err );
	}
	else {
		console.log( sum );		// 7
	}
} );

// get the promise answer
fooPromise
.then(
	function(sum){
		console.log( sum );		// 7
	},
	function(err){
		console.error( err );
	}
);

Both the thunkory and the promisory are essentially asking a question (for a value), and respectively the thunk fooThunk and promise fooPromise represent the future answers to that question. Presented in that light, the symmetry is clear.

With that perspective in mind, we can see that generators which yield Promises for asynchrony could instead yield thunks for asynchrony. All we'd need is a smarter run(..) utility (like from before) that can not only look for and wire up to a yielded Promise but also to provide a callback to a yielded thunk.

Consider:

function *foo() {
	var val = yield request( "http:https://some.url.1" );
	console.log( val );
}

run( foo );

In this example, request(..) could either be a promisory that returns a promise, or a thunkory that returns a thunk. From the perspective of what's going on inside the generator code logic, we don't care about that implementation detail, which is quite powerful!

So, request(..) could be either:

// promisory `request(..)` (see Chapter 3)
var request = Promise.wrap( ajax );

// vs.

// thunkory `request(..)`
var request = thunkify( ajax );

Finally, as a thunk-aware patch to our earlier run(..) utility, we would need logic like this:

// ..
// did we receive a thunk back?
else if (typeof next.value == "function") {
	return new Promise( function(resolve,reject){
		// call the thunk with an error-first callback
		next.value( function(err,msg) {
			if (err) {
				reject( err );
			}
			else {
				resolve( msg );
			}
		} );
	} )
	.then(
		handleNext,
		function handleErr(err) {
			return Promise.resolve(
				it.throw( err )
			)
			.then( handleResult );
		}
	);
}

Now, our generators can either call promisories to yield Promises, or call thunkories to yield thunks, and in either case, run(..) would handle that value and use it to wait for the completion to resume the generator.

Symmetry wise, these two approaches look identical. However, we should point out that's true only from the perspective of Promises or thunks representing the future value continuation of a generator.

From the larger perspective, thunks do not in and of themselves have hardly any of the trustability or composability guarantees that Promises are designed with. Using a thunk as a stand-in for a Promise in this particular generator asynchrony pattern is workable but should be seen as less than ideal when compared to all the benefits that Promises offer (see Chapter 3).

If you have the option, prefer yield pr rather than yield th. But there's nothing wrong with having a run(..) utility which can handle both value types.

Note: The runner(..) utility in my asynquence library, which will be discussed in Appendix A, handles yields of Promises, thunks and asynquence sequences.

Pre-ES6 Generators

You're hopefully convinced now that generators are a very important addition to the async programming toolbox. But it's a new syntax in ES6, which means you can't just polyfill generators like you can Promises (which are just a new API). So what can we do to bring generators to our browser JS if we don't have the luxury of ignoring pre-ES6 browsers?

For all new syntax extensions in ES6, there are tools -- the most common term for them is transpilers, for trans-compilers -- which can take your ES6 syntax and transform it into equivalent (but obviously uglier!) pre-ES6 code. So, generators can be transpiled into code that will have the same behavior but work in ES5 and below.

But how? The "magic" of yield doesn't obviously sound like code that's easy to transpile. We actually hinted at a solution in our earlier discussion of closure-based iterators.

Manual Transformation

Before we discuss the transpilers, let's derive how manual transpilation would work in the case of generators. This isn't just an academic exercise, because doing so will actually help further reinforce how they work.

Consider:

// `request(..)` is a Promise-aware Ajax utility

function *foo(url) {
	try {
		console.log( "requesting:", url );
		var val = yield request( url );
		console.log( val );
	}
	catch (err) {
		console.log( "Oops:", err );
		return false;
	}
}

var it = foo( "http:https://some.url.1" );

The first thing to observe is that we'll still need a normal foo() function that can be called, and it will still need to return an iterator. So, let's sketch out the non-generator transformation:

function foo(url) {

	// ..

	// make and return an iterator
	return {
		next: function(v) {
			// ..
		},
		throw: function(e) {
			// ..
		}
	};
}

var it = foo( "http:https://some.url.1" );

The next thing to observe is that a generator does its "magic" by suspending its scope/state, but we can emulate that with function closure (see the Scope & Closures title of this series). To understand how to write such code, we'll first annotate different parts of our generator with state values:

// `request(..)` is a Promise-aware Ajax utility

function *foo(url) {
	// STATE *1*

	try {
		console.log( "requesting:", url );
		var TMP1 = request( url );

		// STATE *2*
		var val = yield TMP1;
		console.log( val );
	}
	catch (err) {
		// STATE *3*
		console.log( "Oops:", err );
		return false;
	}
}

Note: For more accurate illustration, we split up the val = yield request.. statement into two parts, using the temporary TMP1 variable. request(..) happens in state *1*, and the assignment of its completion value to val happens in state *2*. We'll get rid of that intermediate TMP1 when we convert the code to its non-generator equivalent.

In other words, *1* is the beginning state, *2* is the state if the request(..) succeeds, and *3* is the state if the request(..) fails. You can probably imagine how any extra yield steps would just be encoded as extra states.

Back to our transpiled generator, let's define a variable state in the closure we can use to keep track of the state:

function foo(url) {
	// manage generator state
	var state;

	// ..
}

Now, let's define an inner function called process(..) inside the closure which handles each state, using a switch statement:

// `request(..)` is a Promise-aware Ajax utility

function foo(url) {
	// manage generator state
	var state;

	// generator-wide variable declarations
	var val;

	function process(v) {
		switch (state) {
			case 1:
				console.log( "requesting:", url );
				return request( url );
			case 2:
				val = v;
				console.log( val );
				return;
			case 3:
				var err = v;
				console.log( "Oops:", err );
				return false;
		}
	}

	// ..
}

Each state in our generator is represented by its own case in the switch statement. process(..) will be called each time we need to process a new state. We'll come back to how that works in just a moment.

For any generator-wide variable declarations (val), we move those to a var declaration outside of process(..) so they can survive multiple calls to process(..). But the "block scoped" err variable is only needed for the *3* state, so we leave it in place.

In state *1*, instead of yield request(..), we did return request(..). In terminal state *2*, there was no explicit return, so we just do a return; which is the same as return undefined. In terminal state *3*, there was a return false, so we preserve that.

Now we need to define the code in the iterator functions so they call process(..) appropriately:

function foo(url) {
	// manage generator state
	var state;

	// generator-wide variable declarations
	var val;

	function process(v) {
		switch (state) {
			case 1:
				console.log( "requesting:", url );
				return request( url );
			case 2:
				val = v;
				console.log( val );
				return;
			case 3:
				var err = v;
				console.log( "Oops:", err );
				return false;
		}
	}

	// make and return an iterator
	return {
		next: function(v) {
			// initial state
			if (!state) {
				state = 1;
				return {
					done: false,
					value: process()
				};
			}
			// yield resumed successfully
			else if (state == 1) {
				state = 2;
				return {
					done: true,
					value: process( v )
				};
			}
			// generator already completed
			else {
				return {
					done: true,
					value: undefined
				};
			}
		},
		"throw": function(e) {
			// the only explicit error handling is in
			// state *1*
			if (state == 1) {
				state = 3;
				return {
					done: true,
					value: process( e )
				};
			}
			// otherwise, an error won't be handled,
			// so just throw it right back out
			else {
				throw e;
			}
		}
	};
}

How does this code work?

  1. The first call to the iterator's next() call would move the generator from the uninitialized state to state 1, and then call process() to handle that state. The return value from request(..), which is the promise for the Ajax response, is returned back as the value property from the next() call.
  2. If the Ajax request succeeds, the second call to next(..) should send in the Ajax response value, which moves our state to 2. process(..) is again called (this time with the passed in Ajax response value), and the value property returned from next(..) will be undefined.
  3. However, if the Ajax request fails, throw(..) should be called with the error, which would move the state from 1 to 3 (instead of 2). Again process(..) is called, this time with the error value. That case returns false, which is set as the value property returned from the throw(..) call.

From the outside -- that is, interacting only with the iterator -- this foo(..) normal function works pretty much the same as the *foo(..) generator would have worked. So we've effectively "transpiled" our ES6 generator to pre-ES6 compatibility!

We could then manually instantiate our generator and control its iterator -- calling var it = foo("..") and it.next(..) and such -- or better, we could pass it to our previously defined run(..) utility as run(foo,"..").

Automatic Transpilation

The preceding exercise of manually deriving a transformation of our ES6 generator to pre-ES6 equivalent teaches us how generators work conceptually. But that transformation was really intricate and very non-portable to other generators in our code. It would be quite impractical to do this work by hand, and would completely obviate all the benefit of generators.

But luckily, several tools already exist that can automatically convert ES6 generators to things like what we derived in the previous section. Not only do they do the heavy lifting work for us, but they also handle several complications that we glossed over.

One such tool is regenerator (https://facebook.github.io/regenerator/), from the smart folks at Facebook.

If we use regenerator to transpile our previous generator, here's the code produced (at the time of this writing):

// `request(..)` is a Promise-aware Ajax utility

var foo = regeneratorRuntime.mark(function foo(url) {
    var val;

    return regeneratorRuntime.wrap(function foo$(context$1$0) {
        while (1) switch (context$1$0.prev = context$1$0.next) {
        case 0:
            context$1$0.prev = 0;
            console.log( "requesting:", url );
            context$1$0.next = 4;
            return request( url );
        case 4:
            val = context$1$0.sent;
            console.log( val );
            context$1$0.next = 12;
            break;
        case 8:
            context$1$0.prev = 8;
            context$1$0.t0 = context$1$0.catch(0);
            console.log("Oops:", context$1$0.t0);
            return context$1$0.abrupt("return", false);
        case 12:
        case "end":
            return context$1$0.stop();
        }
    }, foo, this, [[0, 8]]);
});

There's some obvious similarities here to our manual derivation, such as the switch / case statements, and we even see val pulled out of the closure just as we did.

Of course, one trade-off is that regenerator's transpilation requires a helper library regeneratorRuntime that holds all the reusable logic for managing a general generator / iterator. A lot of that boilerplate looks different than our version, but even then, the concepts can be seen, like with context$1$0.next = 4 keeping track of the next state for the generator.

The main takeaway is that generators are not restricted to only being useful in ES6+ environments. Once you understand the concepts, you can employ them throughout your code, and use tools to transform the code to be compatible with older environments.

This is more work than just using a Promise API polyfill for pre-ES6 Promises, but the effort is totally worth it, because generators are so much better at expressing async flow control in a reason-able, sensible, synchronous-looking, sequential fashion.

Once you get hooked on generators, you'll never want to go back to the hell of async spaghetti callbacks!

Review

Generators are a new ES6 function type that does not run-to-completion like normal functions. Instead, the generator can be paused in mid-completion (entirely preserving its state), and it can later be resumed from where it left off.

This pause/resume interchange is cooperative rather than preemptive, which means that the generator has the sole capability to pause itself, using the yield keyword, and yet the iterator that controls the generator has the sole capability (via next(..)) to resume the generator.

The yield / next(..) duality is not just a control mechanism, it's actually a two-way message passing mechanism. A yield .. expression essentially pauses waiting for a value, and the next next(..) call passes a value (or implicit undefined) back to that paused yield expression.

The key benefit of generators related to async flow control is that the code inside a generator expresses a sequence of steps for the task in a naturally sync/sequential fashion. The trick is that we essentially hide potential asynchrony behind the yield keyword -- moving the asynchrony to the code where the generator's iterator is controlled.

In other words, generators preserve a sequential, synchronous, blocking code pattern for async code, which lets our brains reason about the code much more naturally, addressing one of the two key drawbacks of callback-based async.