Flow 0.14.0 包含對產生器函式的支援。產生器函式提供 JavaScript 程式獨特的功能:暫停和繼續執行。這類型的控制為非同步/等待鋪路,這項 即將推出的功能 已受 Flow 支援。
已經有許多精彩的資料說明產生器。我將重點放在靜態型別與產生器的互動。請參閱下列資料以取得有關產生器的資訊
- Jafar Husain 發表了一場 非常清晰且說明詳盡的演講,涵蓋產生器。我已連結到他開始講述產生器的部分,但我強烈建議觀看整場演講。
- Axel Rauschmayer 撰寫的全面性書籍「探索 ES6」,他慷慨地將內容免費提供線上,其中有一 章節說明產生器。
- 備受尊崇的 MDN 有個 實用的頁面,說明
Iterator介面和產生器。
在 Flow 中,Generator 介面有三個型別參數:Yield、Return 和 Next。Yield 是產生器函式產生的值的型別。Return 是產生器函式傳回的值的型別。Next 是透過 Generator 本身的 next 方法傳遞至產生器的值的型別。例如,型別為 Generator<string,number,boolean> 的產生器值會產生 string、傳回 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。我們可以引入一個動態類型測試來讓 Flow 相信不變式,即當 done 為 false 時,value 將始終為 number。
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
上述代碼會產生錯誤,因為 Flow 將 string 和 number 作為下界新增到描述 x 繫結值的類型變數。也就是說,Flow 認為 identity 的類型是 (x: string | number) => string | number,因為這些類型實際上通過了函式。
產生器的另一個重要功能是消費者可以將值傳遞到產生器中。讓我們考慮一個產生器 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);
}
}
我們可以使用這個定義來實作一個命令式總和程序
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 抱怨我們的 next 值可能是 void,而不是預期的 T,也就是 sum 範例中的 number。這種行為對於確保類型安全是必要的。為了啟動產生器,我們的消費者必須先呼叫沒有參數的 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);
}
}
在處理類型化產生器時,還有一個重要的注意事項。從產生器產生的每個值都必須由單一類型描述。類似地,透過 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 表達式的選用回傳型別。
- 避免產生多種型別值的產生器,或使用
any。