Visual Studio Team Servicesのタスクを書いて運用しているが、TypeScript の 非同期周りの仕組みが理解できていないので、いろいろ試して理解してみる。
Callback hell
Javascript や node の界隈で聞かれる言葉で、「Callback hell」というものがある。
例えば、JSONファイルをオープンして読み込むみたいなプログラムを書くとこんな感じになる。
import * as fs from "fs";
function loadJSON(filename: string, callback: (data: any, error:Error) => void){
fs.readFile(filename, function (err, data) {
if (err) {
callback(null , err);
}
else {
try {
console.log(data.toString());
callback (JSON.parse(data.toString()), err);
} catch (err) {
callback (null, err);
}
}
}
)};
console.log("start");
loadJSON("test.json", (data, err) => {
if (err === null) {
console.log(`works: name:${data.name} age:${data.age}`);
} else {
console.log(`error: ${err}`);
}
});
console.log("end");
特にエラー処理の周りが複雑だ。node は非同期処理が基本なので、loadJSONを呼び出している部分のコールバック関数で、さらにコールバックを用いた関数を用いたとき、さらにそこにエラー処理が発生するが、それがネストしてくるとどんどん複雑になってくる。
また、ファイル処理の場合、ファイルを読み込んでから何かの処理をするというケースがあるので順序性が必要になってくるケースも多い。このプログラムを動かしたときの実行結果は下記の通りで、ファイル読み込みより先にend
が出力される。
start
end
{
"name": "Tsuyoshi Ushio",
"age": 46
}
works: name:Tsuyoshi Ushio age:46
順序性を保証するとすると、コールバックで呼ばれた関数の中にその後の処理を書く必要が生じるので、さらにネストは深く複雑に、、、これがコールバックヘルだろう。私もあまりnodeを書いていないのでまだ体験していないので、間違っていたらご指摘いただきたい。
こういった仕組みをシンプルにする仕組みが出てきてそれが、Promise という仕組みらしい。
Promise の考え方とその挙動
Promise は3つの状態を持っている。処理を開始して、終わっていないときは、PENDING、処理が終わってうまくいくと、FULFILLED、失敗すると REJECTED の状態になる。
シンプルなコードを書いてみるとこんな感じ。
function hello(greeting:string) : Promise<any> {
return new Promise((resolve, reject) => {
if (greeting === "hello") {
resolve("hi");
} else {
reject("Say hello!");
}
});
}
hello("hello") // "yo!" に変えると err の方になる
.then((res) => {
console.log(`You said ${res}, right?`);
})
.catch((err) => {
console.log(`${err} Sorry about that`);
});
実行結果は、
You said hi, right?
hello の引数を "yo!" に変更すると
Say hello! Sorry about that
hello メソッドの実行は非同期で、hello を実行した時点では、Promise オブジェクトが返却されて、制御はその先に進む。その時のPromise の状態は、PENDING
resolveが呼ばれた時点で FULLFILL
になり、reject が呼ばれたら REJECTED
になる。
返却されたPromise オブジェクトのthen メソッドは、FULLFILLになった時に呼ばれるもので、catch メソッドは、REJECTEDになったら呼ばれるメソッドになっている。ちなみに、resolve が呼ばれる前に、setTimeout 等で3秒まったりすると、3秒後に、then の中身が実行される。
then や、catch を使うことによって、Promise の戻り値に、順序性を持ったコードが書きやすくなる。例えばこんな感じ。
Promise.resolve(100)
.then((res) => {
console.log(`the 1st value is ${res}`);
return 123;
})
.then((res) => {
console.log(`the 2nd value is ${res}`);
throw new Error("error happened from 2nd.");
})
.then((res) => {
console.log(`the 3rd value is ${res}`);
return 456;
})
.catch((err) => {
console.log(`error: ${err}!!!!`);
return 789;
});
実行結果は
the 1st value is 100
the 2nd value is 123
error: Error: error happened from 2nd.!!!!
最初の行で、Promise がresolve されて 100の値が渡される。then のメソッドで、それを受け取って、メッセージを出しており、123を返却している。この返却値は、Promiseのresovle の引数になり、Promise オブジェクトを返却する。
次のthen はそのPromise オブジェクトを受け取ってメッセージを表示するが、次はエラーを発生させている。これも、rejectedが呼ばれたPromise オブジェクトになって返却される。次のthen は、FULLFILL の場合のみだからスキップされて、最後のcatch が呼ばれる。という流れ。このメソッドのチェーンがあれば結構いい感じで最初のプログラムがかけるとのこと。
最初のプログラムに戻って
さて、Promise を覚えたので、Promise で最初のプログラムを自分的に書き直してみた。
import * as fs from "fs";
function loadJSON(filename:string): Promise<any> {
return new Promise ((resolve, reject) => {
fs.readFile(filename, function (err, data){
if (err) {
reject(err);
} else {
try {
resolve(JSON.parse(data.toString()));
} catch (err) {
reject (err);
}
}
});
});
};
console.log("start");
loadJSON("test.json").then((data) => {
console.log(`works: name:${data.name} age:${data.age}`);
}).catch((err) => {
console.log(`error: ${err}`);
});
console.log("end");
うん。すっきりした。が、Promise のドキュメントを参照してみると、これはまだダサコードのようだ。そのドキュメントをヒントにさらにリファクタリング
import * as fs from "fs";
function readFileAsync(filename: string): Promise<any> {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, result) => {
if (err) reject(err);
else resolve(result);
})
})
}
function loadJSONAsync(filename:string): Promise<any> {
return readFileAsync(filename)
.then(function (res) {
return JSON.parse(res);
});
};
console.log("start");
loadJSONAsync("test.json").then((data) => { // "test.json"を"tests.json" に変えたらエラーになる
console.log(`works: name:${data.name} age:${data.age}`);
}).catch((err) => {
console.log(`error: ${err}`);
});
console.log("end");
カッケー!中間の部分がPromise のメソッドチェーンで、エラー処理を一元化できている!まるでMaybeモナドのようだ!
実行結果はもちろん同じ
start
end
works: name:Tsuyoshi Ushio age:46
ファイル名を架空にすると
start
end
error: Error: ENOENT: no such file or directory, open 'C:\Users\tsushi\Codes\typescript\a
wait\tests.json'
しっかりエラー! Promise 先輩カッコいいっす!さて、これが、どうasync/await につながるかは次のブログで