Flow 0.14.0 には、ジェネレーター関数のサポートが含まれています。ジェネレーター関数は、JavaScript プログラムに独自の機能を提供します。それは、実行を一時停止および再開する機能です。この種の制御は、async/await(Flow で既にサポートされている、今後の機能)への道を開きます。
ジェネレーターを説明する素晴らしい資料は既にたくさん作成されています。ここでは、静的型付けとジェネレーターの相互作用に焦点を当てます。ジェネレーターに関する情報については、以下の資料を参照してください。
- Jafar Husain は、ジェネレーターをカバーする非常に明快で分かりやすい講演を行いました。ジェネレーターについて触れている箇所にリンクしましたが、講演全体を強くお勧めします。
- Axel Rauschmayer による包括的な書籍『Exploring ES6』は、内容がオンラインで無料で公開されており、ジェネレーターに関する章があります。
- 由緒ある MDN には、
Iterator
インターフェースとジェネレーターについて説明した便利なページがあります。
Flow では、Generator
インターフェースには Yield
、Return
、Next
の 3 つの型パラメーターがあります。Yield
は、ジェネレーター関数から yield される値の型です。Return
は、ジェネレーター関数から返される値の型です。Next
は、Generator
自体の next
メソッドを介してジェネレーターに渡される値の型です。たとえば、Generator<string,number,boolean>
型のジェネレーター値は、string
を yield し、number
を返し、呼び出し元から boolean
を受け取ります。
任意の型 T
に対して、Generator<T,void,void>
は、Iterable<T>
と Iterator<T>
の両方です。
ジェネレーターの独自性により、無限シーケンスを自然に表現できます。自然数の無限シーケンスを考えてみましょう。
function *nats() {
let i = 0;
while (true) {
yield i++;
}
}
ジェネレーターはイテレーターでもあるため、手動でジェネレーターを反復処理できます。
const gen = nats();
console.log(gen.next()); // { done: false, value: 0 }
console.log(gen.next()); // { done: false, value: 1 }
console.log(gen.next()); // { done: false, value: 2 }
done
が false の場合、value
はジェネレーターの Yield
型になります。done
が true の場合、value
はジェネレーターの Return
型になるか、コンシューマーが完了値を過ぎて反復処理した場合は void
になります。
function *test() {
yield 1;
return "complete";
}
const gen = test();
console.log(gen.next()); // { done: false, value: 1 }
console.log(gen.next()); // { done: true, value: "complete" }
console.log(gen.next()); // { done: true, value: undefined }
この動作のため、手動で反復処理すると型付けが難しくなります。手動の反復処理によって nats
ジェネレーターから最初の 10 個の値を取得してみましょう。
const gen = nats();
const take10: number[] = [];
for (let i = 0; i < 10; i++) {
const { done, value } = gen.next();
if (done) {
break;
} else {
take10.push(value); // error!
}
}
test.js:13
13: const { done, value } = gen.next();
^^^^^^^^^^ call of method `next`
17: take10.push(value); // error!
^^^^^ undefined. This type is incompatible with
11: const take10: number[] = [];
^^^^^^ number
Flow は、value
が undefined
になる可能性があると警告しています。これは、value
の型が Yield | Return | void
であり、nats
のインスタンスでは number | void
に簡略化されるためです。動的な型テストを導入して、done
が false の場合、value
は常に number
になるという不変条件を Flow に納得させることができます。
const gen = nats();
const take10: number[] = [];
for (let i = 0; i < 10; i++) {
const { done, value } = gen.next();
if (done) {
break;
} else {
if (typeof value === "undefined") {
throw new Error("`value` must be a number.");
}
take10.push(value); // no error
}
}
タグ付きユニオンを改良するための番兵として done
値を使用することにより、上記の動的な型テストを不要にする未解決の問題があります。つまり、done
が true
の場合、Flow は value
が常に Yield
型であり、それ以外の場合は Return | void
型であることを認識します。
動的な型テストがなくても、このコードは非常に冗長で、意図が分かりにくいものです。ジェネレーターはイテラブルでもあるため、for...of
ループも使用できます。
const take10: number[] = [];
let i = 0;
for (let nat of nats()) {
if (i === 10) break;
take10.push(nat);
i++;
}
これはかなり良いでしょう。for...of
ループ構造は完了値を無視するため、Flow は nat
が常に number
になることを理解します。ジェネレーター関数を使用して、このパターンをさらに一般化してみましょう。
function *take<T>(n: number, xs: Iterable<T>): Iterable<T> {
if (n <= 0) return;
let i = 0;
for (let x of xs) {
yield x;
if (++i === n) return;
}
}
for (let n of take(10, nats())) {
console.log(n); // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
}
take
ジェネレーターのパラメーターと戻り値の型を明示的に注釈していることに注意してください。これは、Flow が完全にジェネリックな型を理解できるようにするために必要です。これは、Flow が現在、完全にジェネリックな型を推論せず、代わりに下限を累積して、ユニオン型にするためです。
function identity(x) { return x }
var a: string = identity(""); // error
var b: number = identity(0); // error
上記のコードは、string
と number
が、x
によってバインドされた値の型を記述する型変数への下限として Flow が追加するため、エラーが発生します。つまり、Flow は、identity
の型が (x: string | number) => string | number
であると信じています。これは、実際にその関数を通過した型であるためです。
ジェネレーターのもう 1 つの重要な機能は、コンシューマーからジェネレーターに値を渡す機能です。提供された関数を使用してジェネレーターに渡された値を減らすジェネレーター scan
を考えてみましょう。この scan
は Array.prototype.reduce
に似ていますが、各中間値を返し、値は next
を介して命令的に提供されます。
最初のパスとして、これを次のように記述できます。
function *scan<T,U>(init: U, f: (acc: U, x: T) => U): Generator<U,void,T> {
let acc = init;
while (true) {
const next = yield acc;
acc = f(acc, next);
}
}
この定義を使用して、命令型 sum プロシージャを実装できます。
let sum = scan(0, (a,b) => a + b);
console.log(sum.next()); // { done: false, value: 0 }
console.log(sum.next(1)); // { done: false, value: 1 }
console.log(sum.next(2)); // { done: false, value: 3 }
console.log(sum.next(3)); // { done: false, value: 6 }
ただし、上記の scan
の定義をチェックしようとすると、Flow は警告を表示します。
test.js:7
7: acc = f(acc, next);
^^^^^^^^^^^^ function call
7: acc = f(acc, next);
^^^^ undefined. This type is incompatible with
3: function *scan<T,U>(init: U, f: (acc: U, x: T) => U): Generator<U,void,T> {
^ some incompatible instantiation of T
Flow は、sum
の例では number
であるはずの予期された T
ではなく、値 next
が void
である可能性があると警告しています。この動作は、型安全性を確保するために必要です。ジェネレーターを準備するには、コンシューマーは最初に引数なしで next
を呼び出す必要があります。これを考慮して、Flow は next
の引数をオプションとして認識します。これは、Flow が次のコードを許可することを意味します。
let sum = scan(0, (a,b) => a + b);
console.log(sum.next()); // first call primes the generator
console.log(sum.next()); // we should pass a value, but don't need to
一般に、Flow はどの呼び出しが「最初」であるかを認識しません。最初の next
に値を渡すことはエラーであり、後続の next
に値を渡さないこともエラーである必要がありますが、Flow は妥協して、ジェネレーターに潜在的な void
値に対処することを強制します。要するに、型 Generator<Y,R,N>
のジェネレーターと型 Y
の値 x
が与えられた場合、式 yield x
の型は N | void
です。
実行時に void
でない不変条件を強制する動的な型テストを使用するように定義を更新できます。
function *scan<T,U>(init: U, f: (acc: U, x: T) => U): Generator<U,void,T> {
let acc = init;
while (true) {
const next = yield acc;
if (typeof next === "undefined") {
throw new Error("Caller must provide an argument to `next`.");
}
acc = f(acc, next);
}
}
型付きジェネレーターを扱う場合、もう 1 つ重要な注意点があります。ジェネレーターから yield されるすべての値は、単一の型で記述する必要があります。同様に、next
を介してジェネレーターに渡されるすべての値は、単一の型で記述する必要があります。
次のジェネレーターを考えてみましょう。
function *foo() {
yield 0;
yield "";
}
const gen = foo();
const a: number = gen.next().value; // error
const b: string = gen.next().value; // error
これは完全に有効な JavaScript であり、値 a
と b
は実行時に正しい型になります。ただし、Flow はこのプログラムを拒否します。ジェネレーターの Yield
型パラメーターには、number | string
の具体的な型があります。イテレーターの結果オブジェクトの value
プロパティの型は number | string | void
です。
ジェネレーターに渡された値についても同様の動作を観察できます。
function *bar() {
var a = yield;
var b = yield;
return {a,b};
}
const gen = bar();
gen.next(); // prime the generator
gen.next(0);
const ret: { a: number, b: string } = gen.next("").value; // error
値 ret
には、実行時に注釈された型がありますが、Flow もこのプログラムを拒否します。ジェネレーターの Next
型パラメーターには、number | string
の具体的な型があります。したがって、イテレーターの結果オブジェクトの value
プロパティの型は void | { a: void | number | string, b: void | number | string }
になります。
動的な型テストを使用してこれらの問題を解決することは可能ですが、別の実用的なオプションとして、any
を使用して型安全性の責任を自分で負うことができます。
function *bar(): Generator {
var a = yield;
var b = yield;
return {a,b};
}
const gen = bar();
gen.next(); // prime the generator
gen.next(0);
const ret: void | { a: number, b: string } = gen.next("").value; // OK
(注釈 Generator
は Generator<any,any,any>
と同等です。)
ふう!これが、独自のコードでジェネレーターを使用するのに役立つことを願っています。また、JavaScript のような非常に動的な言語に静的分析を適用することの難しさについて少し洞察が得られたことを願っています。
要約すると、静的に型付けされた JS でジェネレーターを使用するための教訓は次のとおりです。
- ジェネレーターを使用して、カスタム イテラブルを実装します。
- 動的な型テストを使用して、yield 式のオプションの戻り値の型をアンパックします。
- 複数の型の値の yield または受信を行うジェネレーターは避けるか、
any
を使用します。