ES2016におけるasyncとgenerator、Promise、CPSの関係
次の非同期sleepをES2016ではどう書けるかを比較する。
// sleep: number -> Promise<number>
function sleep(ms){
return new Promise(resolve =>
setTimeout((()=>resolve(ms)), ms));
}
async-await版
async function main(){
let a = await sleep(1000);
alert(`${a}ms passed`); // 1000ms passed
let b = await sleep(2000);
alert(`${b}ms passed`); // 2000ms passed
let [c, d] = await Promise.all([
sleep(3000),
sleep(4000)
]);
alert(`${Math.max(c, d)}ms passed`); // 4000ms passed
alert('done');
}
main(); // return Promise
async-await記法を使うとスッキリ書ける。
非同期処理の同期待ちには Promise.all
を await
すればよい。
このコードはgeneratorを使うと次のように書ける。
generator版
// main: void -> Promise<void>
let main = async(function* _main(){
let a = yield sleep(1000);
alert(`${a}ms passed`);
let b = yield sleep(2000);
alert(`${b}ms passed`);
let [c, d] = yield Promise.all([
sleep(3000),
sleep(4000)
]);
alert(`${Math.max(c, d)}ms passed`);
alert('done');
});
// async: (void -> Generator) -> (void -> Promise)
function async(generatorFunc) {
let generator = generatorFunc();
let onResolved = arg =>{
let result = generator.next(arg);
if (result.done) {
return result.value;
} else {
return Promise
.resolve(result.value)
.then(onResolved);
}
}
return onResolved;
}
main(); // return Promise<void>
generator関数である _main
が yeild sleep(3000)
で Promise<number>
返す。
その Promise<number>
を async
関数が成功か失敗かを判断し、成功ならば次の計算を呼び出すことで、async-awaitとおなじようにフラットに書ける。
つまり async
関数は 内部で Promise のチェーンをつないでいるのだ。
Promise版
// main: void -> Promise<void>
function main(){
return sleep(1000).then((a)=>{
alert(`${a}ms passed`);
return sleep(2000).then((b)=>{
alert(`${b}ms passed`);
return Promise.all([sleep(3000), sleep(4000)]).then(([c, d])=>{
alert(`${Math.max(c, d)}ms passed`);
alert('done');
return;
});
});
});
});
main(); // return Promise<void>
これが generator 版で async
関数が作っていた Promise のチェーンである。
重要な事実だが Promise では コールバック地獄を防ぐことはできない 。
async function main(){
const a = await getA();
const b = await getB();
return a + b;
}
のような処理を Promise で書こうとすると
function main(){
return getA().then((a)=>
getB().then((b)=>
a + b));
}
というように以前の前の Promise の返り値を利用するためにはクロージャを利用する必要があるため
ネストを深くせざるを得ないからである。
ではなぜ Promise をつかうのかというと、 then メソッドや Promise.all, Promise.race を使うことで非同期処理の演算ができるようになり、コールバックスタイルよりも表現力が上がるからである。
最後に sleep
を古き良きコールバックスタイルにし、これを継続渡し形式(CPS)で書いてみる。
CPS版
// sleep: number -> (number -> void) -> void
function sleep(ms, cb){
setTimeout((()=> cb(ms)), ms);
return;
}
// main: void -> Promise<void>
function main(){
sleep(1000, (a)=>{
alert(`${a}ms passed`);
sleep(2000, (b)=>{
alert(`${b}ms passed`);
let waitAll = genWaitAll(next);
sleep(3000, waitAll());
sleep(4000, waitAll());
function next([c, d]){
alert(`${Math.max(c, d)}ms passed`);
alert('done');
return;
}
});
});
};
// genWaitAll: ([a] -> void) -> (void -> (a -> void))
function genWaitAll(next){
let results = [];
let counter = 0
return ()=>{
let i = counter;
counter++;
return (ms)=>{
results[i] = ms;
counter--;
if (counter === 0){
next(results);
}
};
}
};
main(); // return void;
callback hell が起きている。
また、 Promise.all
のやっていた同期待ち処理を genWaitAll
のように手書きする必要がある。
モナドとか
JavaScript + generator で Maybe、 Either、 Promise モナドと do 構文を実装し async-await と比べてみる