銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

線形回帰で「アヤメ(iris)」の分類

f:id:takamints:20171015205935j:plain

octaveを使って線形回帰で「アヤメの分類」をやってみました。結果的には「これ間違ってなくない?」って感じの中途半端な状態です。なんだかイケそうな気がしたのですが、残念です。しかしまあ、せっかくですから誰かの何かの足しになれば?と書いておきます。

なんで急にこんなことやり始めたかっていうとですね、先日、会社の方に初めての機械学習関連のお仕事が舞い込んできたようで、自分は担当外なのですが「今の時代、それぐらいできなアカンやろーwww」とか、いつもの調子でついハッタリをかましてしまって(笑)・・・、
不安になって自宅で機械学習のコソ練せざるを得ないという。いくつになってもお勉強です(泣)

「線形回帰てなんやねん」については以下の記事で、ごく初歩の概念を説明してます。
takamints.hatenablog.jp

お仕事の方は、Chainer/XGBoost/Pythonという条件がついているようですが、我が師と(勝手に)仰いでいる Andrew Ng 先生は「最初は octave(matlab) でやりなはれや」とおっしゃられていたので、手元の octave でやってみましたという次第。

ということで、以降のスクリプトを実行するには octave または matlabが必要です。 データセットのダウンロードと変換に curl と Nodeを使っていますが、手動でやるなら不要ですよ。

Chainer v2による実践深層学習
新納 浩幸
オーム社
売り上げランキング: 4,654
MATLABとOctaveによる科学技術計算

丸善出版
売り上げランキング: 130,528

まずはアヤメ(iris)をダウンロードして変換します

機械学習のためのオープンな訓練データとして、アヤメ(iris)というのがあるらしく、データセットは150件と小さくて、4つのパラメータで3種類のあやめを同定するというものです。

上記のファイルをダウンロードするため以下のシェルスクリプトを書きました。 iris.dataをダウンロードしてCSVを変換しています(変換については次項↓参照)。

get-iris-csv.sh

#!/bin/sh
echo Downloading iris.data and iris.names.
curl --silent https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data > iris.data
curl --silent https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.names > iris.names
echo
echo Converting iris.data CSV.
echo
node conv-iris-csv.js

bashで実行するとこうなります。

$ ./get-iris-csv.sh
Downloading iris.data and iris.names.

Converting iris.data CSV.

The class values in column 5:
0: Iris-setosa
1: Iris-versicolor
2: Iris-virginica

CSVの5列目を数値に変換

元々 iris.data の5列目は文字列(アヤメの種類の名称)なのですが、octave の csvread 関数では数値しか読み込めません。行列ですからね。 そこで以下のNodeスクリプトで iris.data を読み込んですべての行の5列目の文字列を 0,1,2 の数値に変換して、iris.csv に出力してます (おっと、エラーを一切見ていませんが良い子は絶対マネしないで)。

conv-iris-csv.js

#!/usr/bin/env node
var inputFile = "iris.data";
var outputFile = "iris.csv";
var fs = require("fs");
fs.readFile(inputFile, "utf-8", function(err, data) {
    var indexOfName = {};
    fs.writeFile(outputFile, data.split(/\r*\n/).map(function(row, indexRow) {
        if(row == "") {
            return "";
        }
        return row.split(",").map(function(column, indexCol) {
            if(indexCol < 4) {
                return column;
            }
            if(!(column in indexOfName)) {
                indexOfName[column] = Object.keys(indexOfName).length;
            }
            return indexOfName[column];
        }).join(",");
    }).join("\n"),
    function(err) {
        var nameIndex = new Array(Object.keys(indexOfName).length);
        Object.keys(indexOfName).forEach(function(key) {
            nameIndex[indexOfName[key]] = "" + indexOfName[key] + ": " + key;
        });
        console.log("The class values in column 5:");
        console.log(nameIndex.join("\n"));
    });
});

線形回帰のoctave/matlabスクリプト

以下のoctave/matlabスクリプトが今回の御本尊。 毎度長くてごめんなさい。

  1. CSVを読み込んで、
  2. 学習してから
  3. 検証してます。

本来ならばデータセットの3分の1程度をトレーニングには使わず検証用に残しておくべきなのですが、 データ件数が150件と非常に小さいため、全データでトレーニングして、全データで検証してます。手前味噌な感じですが仕方がない。

iris.m

#
# Classification of iris using liner regression
#

# Clear all mat
clear

D = csvread('iris.csv');
D = D(randperm(size(D,1)),:);   # Sort random

#
# Select training dataset
#
Dt = D(1:150, :);               # Dt = D(1:100, :);
Xt = Dt(:, 1:4);    # Input
Xt = [ones(size(Xt,1),1) Xt];
Yt = Dt(:, 5);      # Result

#
# Select validation dataset
#
Dv = D(1:150, :);               # Dv = D(101:150, :);
Xv = Dv(:, 1:4);
Xv = [ones(size(Xv,1),1) Xv];
Yv = Dv(:, 5);

training_data_count = size(Xt,1);
learning_rate = 0.005     # learning rate
iteration = 100000

#
# Training
#
theta = [1,1,1,1,1]; # factor
numOfParam = size(theta, 2);
update = [1, numOfParam];
for n = 1:iteration
    diff = Xt * theta' - Yt;
    for j = 1:numOfParam
        update(1,j) = sum((diff .* Xt(:,j)));
    endfor
    theta = theta - learning_rate * update / training_data_count;
    if mod(n,10000) == 0
        cost = sum(diff .* diff) / ( 2 * size(Xt,1) )
    endif
endfor

#
# Validation
#
y = Xv * theta';
diff = y - Yv;
#validationResult = [Yv round(y)]

error_count = 0;
for i = 1:size(diff,1)
    diffI = diff(i,1);
    if(diffI * diffI >= 0.5 * 0.5)
        error_index = i
        error_count = error_count + 1;
    endif
endfor
theta
validationErrorRate = error_count / size(Xv, 1)
validationTotalCost = sum(diff .* diff) / ( 2 * size(Xv,1) )

結果検証

以下が上記スクリプトの実行結果。エラー率が2.7%となかなか高い。 100件あたり3件程度は間違うってことですから、おそらく実務では使えないですね。

learning_rate =  0.0050000 # 学習レート
iteration =  100000        # 学習回数
cost =  0.023792
cost =  0.023476
cost =  0.023328
cost =  0.023257
cost =  0.023223
cost =  0.023207
cost =  0.023200
cost =  0.023196
cost =  0.023194
cost =  0.023193
error_index =  30  #判定エラー1個目
error_index =  101 #判定エラー2個目
error_index =  118 #判定エラー3個目
error_index =  120 #判定エラー4個目
theta =

   0.206248  -0.111832  -0.045483   0.227252   0.610735

validationErrorRate =  0.026667
validationTotalCost =  0.023193

中途半端で残念ですが・・・

leaning_rateと学習回数を変化させても、残念ながら、これ以上エラー率が下がりませんでした。

ところで、iris.namesの「4. Relevant Information」に、 「ひとつは他の2つからリニアに分離できるけど他の2つは無理」と書いてあって(以下1>の行)、そもそも線形回帰では無理なのでは?とか思っています。

それから「データに間違いがある」とも書かれていて(以下2>の行)、該当箇所を修正してみたのですけど、大して結果は変わりませんでした。

4. Relevant Information:
   --- This is perhaps the best known database to be found in the pattern
       recognition literature.  Fisher's paper is a classic in the field
       and is referenced frequently to this day.  (See Duda & Hart, for
       example.)  The data set contains 3 classes of 50 instances each,
1>       where each class refers to a type of iris plant.  One class is
1>       linearly separable from the other 2; the latter are NOT linearly
1>       separable from each other.
   --- Predicted attribute: class of iris plant.
   --- This is an exceedingly simple domain.
   --- This data differs from the data presented in Fishers article
    (identified by Steve Chadwick,  spchadwick@espeedaz.net )
2>   The 35th sample should be: 4.9,3.1,1.5,0.2,"Iris-setosa"
2>   where the error is in the fourth feature.
2>   The 38th sample: 4.9,3.6,1.4,0.1,"Iris-setosa"
2>   where the errors are in the second and third features.  ```

まあ、そもそも線形回帰のモデルをワタシが間違っているかもしれません。データモデルが単純過ぎる? だいたい150件ってトレーニングデータとして少なすぎる気もするのですが・・・。

とりあえず、半日かけての中途半端な成果は以下のGitリポジトリに置いてます。

github.com

今後の展望

結果が離散的な分類問題ですから、本来はロジスティック回帰で解く必要があるのかも?と思っています。 さらに、ニューラルネットワークなら簡単じゃね?とかよくわからないけど希望的観測も(バックプロパゲーション信仰w)。

なので、そのうち試してみようと思います。

データ解析のためのロジスティック回帰モデル
Jr David W. Hosmer Stanley Lemeshow Rodney X. Sturdivant
共立出版
売り上げランキング: 178,009

C++でJsonを扱うクラスライブラリ jsonexpr

8月から支援に入った画像処理(リアルタイム・オブジェクト・トラッキング)プロジェクトの検証システムで、C++の本体からJSONを吐く必要がありまして、急いでやっつけたのですが、「そういや昔、C++JSON扱うクラスライブラリ作ったね・・・」と思い出しました(遅い)。

ということで、古いGitHubから引っ張り出して確認すると、どうにか使えそうなので、この場を借りて宣伝します(笑)

f:id:takamints:20170924182608p:plain

実践OpenCV 3 for C++画像映像情報処理
永田 雅人 豊沢 聡
カットシステム
売り上げランキング: 263,159

元々 Node.jsで出力したJSONファイルをC++で読み込んで処理させる目的で作ったものです。 当時、ニューラルネットワークバックプロパゲーションを理解するための実験を何故かC++で書いていましたが、Node.jsで大量に生成した訓練用データを入力する必要があったのです。結局本体もJavaScriptに移植したので存在を忘れてた。

C++で学ぶディープラーニング
藤田 毅
マイナビ出版 (2017-06-26)
売り上げランキング: 192,886

機能

JavaScriptの変数と同じように扱うためのjson::varというクラスが、ほぼすべての機能を含んでいます。他のクラスはこのクラスの内部で使用するものです。

ビルドについて

ビルドはCMakeを使っています。手元の環境では、Visual Studio 2015のソリューションは生成できました。Mingw32のG++用スクリプトが入っていますがエラーになります(原因は追求していません)。Linuxでは試していませんが、またそのうち。

ライセンス

ライセンスを明記していなかったので急遽MITライセンスを謳っておきました。

リポジトリはコチラです

github.com

同カテゴリでは picojson など著名なライブラリがいくつかあるようですが、興味のある方は試してみて下さい。 今見ると少し手を入れたい所がありますが、取り急ぎ現状のままお送りします。

以下、READMEを貼っておきます。

jsonexpr

JSONC++で自然に扱うために作ったライブラリです。

json::var クラスのインスタンスが、JavaScript のデータ型(Number, boolean, string, Array, Object)に相当します。

データ型

  1. 数値 - double型の数値
  2. 文字列 - std::string型の文字列
  3. 配列 - json::var型の配列
  4. オブジェクト - 文字列からjson::var型へのディクショナリ

初期化

json::var のコンストラクタにJSON文字列を与えて初期化できます。 プリミティブ型ではあとで説明していますが、代入するほうが直感的かも知れませんね。

//数値の初期化(内部表現は全てdouble)
json::var real("-1.234e+5");  //浮動小数点
json::var dec("1234");        //10進整数
json::var hex("0x1234");      //16進整数
json::var oct("0644");        //8進整数

//文字列で初期化
json::var str("'string'");    //シングルクォート

//配列の初期化
json::var arr("[-1.234e+5,'string']");

//オブジェクトの初期化
json::var obj("{'key':'value', foo: 'bar', arr:[1,2,3,4]}");

配列操作

  1. length() - 要素数を得る
  2. push(値) - 要素の追加。値は数値、文字列、配列、オブジェクト
  3. remove(index) - 要素の削除。
json::var arr("[]");//空の配列
arr.push(1.0);      //数値を追加
arr.push("string"); //文字列を追加
for(int i = 0; i < arr.length(); i++) {
    cout << arr[i];
}

オブジェクト操作

  1. exists(key) - キーの有無を調べる
  2. keys() - キーの配列を返す。
  3. remove(key) - キーの削除。

新たなキーに値を関連付けるには、[]で直接代入します。 ※ 定数でないoperator[](const std::string&)では、参照するだけでキーが作成されることに注意。

json::var obj("{}");    //空のオブジェクト
obj["A"] = "B";         //文字列の値を追加
obj["R"] = arr;         //既にある配列のコピーを追加
json::var keys = obj.keys();
for(int i = 0; i < keys.length(); i++) {
    cout << "#" << keys[i] << " => " << obj[keys[i]] << endl;
}

値の参照

数値型は、doubleへのキャスト、文字列型は std::string&へのキャストで値を参照します。

配列要素とオブジェクトの値は[]を使用。配列要素は添え字が整数、オブジェクトは文字列型。 ネストしたオブジェクトは二次元配列のようにかぎかっこ([])をつないで参照します。

json::var real = -1.234e+5;
double r = (double)real;     // r == -1.234e+5;

json::var str = "ABC";
std::string s = (const std::string&)str;

json::var element = arr[3];             //配列要素の参照
json::var value = obj["arr"][0];      //オブジェクト内の配列要素の参照

内部表現と矛盾する参照を行うと、例外が投入されます。

値の代入

data = 1.234;           //数値を代入
data = "string";        //文字列を代入
arr[3] = 1.234;         //配列要素の書き換え
obj["key"] = 1.234;     //オブジェクト要素の書き換え
obj["new key"] = arr;   //オブジェクトへ新たな項目を追加

JSONデータのストリーム入出力

C++の標準ストリーム入出力機能を実装しています。

std::istream からのJSONの読み込み

json::var dataobj;
ifstream is("input.json");
is >> dataobj;

std::ostreamへのJSON出力

json::var dataobj;
ofstream is("output.json");
os << dataobj;

制限事項

javascriptの仕様に完全準拠しているわけではありません。

  • NaNを扱えません。
  • Functionを扱えません。
  • オブジェクトのキーは文字列だけです。
  • 数値は内部的にdoubleだけです。

長いサンプル

    // json 文字列からの構築
    json::var num("-1.2345e+3");
    json::var str("'this is string.'");
    json::var arr("[ \"key\", 'str', 'hex', 0xABCD, 0777 ]");
    json::var obj("{ foo : 'bar', 'boo':1.2345e-6, 'arr': [0,1,2,3]}");

    // 参照
    double numval = num;
    string strval = str;
    cout << numval << endl;
    cout << strval << endl;

    numval = arr[3];
    cout << numval << endl;

    strval = (string)obj["foo"];
    cout << strval << endl;

    strval = (string)obj["arr"][2];
    cout << strval << endl;

    // 配列の扱い
    cout << "arr.length() = " << arr.length() << endl;
    arr.push(9.876);
    arr.push("string element");
    arr.push(obj);
    arr.push(arr);
    cout << "arr.length() = " << arr.length() << endl;

    //
    // オブジェクトへ新たなキーを追加
    //
    cout << "obj.exists('new key') = " << (obj.exists("new key")?"true":"false") << endl;
    obj["new key"] = json::var("[0,1,2,3]");
    cout << "obj.exists('new key') = " << (obj.exists("new key")?"true":"false") << endl;

    //
    // std::ostreamへのJSONの書き出し
    //
    ostringstream os;
    os << obj;

    //
    // std::istreamからJSONを読み出し
    //
    istringstream is("{ foo : 'bar', 'boo':1.2345e-6, 'arr': [9,8,7,6] }");
    is >> obj;

    //
    // 別の値(別の型)に書き換え
    //
    num = 4.0;
    cout << num << endl;

    num = "overwrite string";
    cout << num << endl;

    num = obj;
    cout << num["boo"] << endl;

    num["boo"] = "change by reference";
    cout << num["boo"] << endl;

    num["arr"][1] = "change by reference";
    cout << num["arr"][1] << endl;

LICENSE

このソフトウェアは、MIT ライセンスにて、提供します。LICENSE を参照下さい。

This software is released under the MIT License, see LICENSE

Node.js+Sqlite(npm sqlite3)のeachメソッドは中断できない

f:id:takamints:20170829222928p:plain
photo credit: Skakerman 39/52 - Signs via photopin (license)

8月初旬に急遽「C++で画像処理(オブジェクトトラッキング)」ってなプロジェクト(しかもOpenCVを使わないという骨太方針w)のヘルプに駆り出されて、それまでやってた「Node.js+sqlite3」の悩みどころや困りどころを、今やスッカリ忘れそうになっているので、思い出しつつ書いておきます。

[改訂第4版]SQLポケットリファレンス
朝井 淳
技術評論社
売り上げランキング: 95,176

sqlite3のeachメソッドは中断できない

表題と同じですけど大切なことなので2回・・・

ええ、クエリ結果セットから一件ずつレコードを読むための Database#each または、 Statement#each メソッドなんですが、 どちらも公式ドキュメントに「今のところ中断できない」と書いてあって実質的に使えないです。

Database#each

Database#each(sql, [param, …], [callback], [complete])

Runs the SQL query with the specified parameters and calls the callback once for each result row. The function returns the Database object to allow for function chaining. The parameters are the same as the Database#run function, with the following differences:

The signature of the callback is function(err, row) {}. If the result set succeeds but is empty, the callback is never called. In all other cases, the callback is called once for every retrieved row. The order of calls correspond exactly to the order of rows in the result set.

After all row callbacks were called, the completion callback will be called if present. The first argument is an error object, and the second argument is the number of retrieved rows. If you specify only one function, it will be treated as row callback, if you specify two, the first (== second to last) function will be the row callback, the last function will be the completion callback.

If you know that a query only returns a very limited number of rows, it might be more convenient to use Database#all to retrieve all rows at once.

There is currently no way to abort execution.

これ見落としていて、えらい目に遭いました。まさかこんなしれっと書いてあるなんて。

Statement#each(↓)なんて、なぜだかビックリマーク付きですよ。

Statement#each

Statement#each([param, …], [callback], [complete])

Binds parameters, executes the statement and calls the callback for each result row. The function returns the Statement object to allow for function chaining. The parameters are the same as the Statement#run function, with the following differences:

The signature of the callback is function(err, row) {}. If the result set succeeds but is empty, the callback is never called. In all other cases, the callback is called once for every retrieved row. The order of calls correspond exactly to the order of rows in the result set.

After all row callbacks were called, the completion callback will be called if present. The first argument is an error object, and the second argument is the number of retrieved rows. If you specify only one function, it will be treated as row callback, if you specify two, the first (== second to last) function will be the row callback, the last function will be the completion callback.

Like with Statement#run, the statement will not be finalized after executing this function.

If you know that a query only returns a very limited number of rows, it might be more convenient to use Statement#all to retrieve all rows at once.

There is currently no way to abort execution!

結局 all と同じですからー

通常この手のメソッドは、SQLを発行してフェッチした行を順次1行ずつコールバックで通知してくれて、 「もう十分。これ以上必要ない」って時に中断したいわけですが、それが(今のところは)できないってこと。 つまり結局最後まで読みきらないといけないってことで、それなら Database#allStatement#allと同じですよね。

LIMIT使えばいいじゃない / allだけで行けるじゃない?

結局、同じようなことをするためには、SQLでLIMIT句を使って、allメソッドでページングしながら、複数行を繰り返し取ってくるしか手はありませんでした。

結果は大体同じですけど、SQLを複数回発行することになるので、パフォーマンスはちょっと低下すると思います。

LIMIT句をパラメタライズしてStatementを用意しておけば、あまり気にならないとは思いますが。

ま、巨大な結果セットを返さないようにSQLを変更できるなら、それを優先する必要がありますね。

ここぞとコントリビュートしなさいよ

オープンソースなんだから「使えねー」とか文句ばっか言ってないで、フォークして対応してプルリク投げればよいのですが、、、

コレぐらいバカでかいプロジェクトだとちょっと躊躇しますね(笑)

今のところ画像処理方面でアレなんで(言い訳)、またそのうち・・・。

github.com

意外に悩ましい整数部分の四捨五入

f:id:takamints:20170718211752p:plain
photo credit: danmachold calculator via photopin (license)

JavaScriptで整数部分を四捨五入する場合の注意点。桁落ちに気を付けましょうってお話です。

JavaScriptで、小数を整数に四捨五入するにはMath.roundを使いますね。

でもこれ、残念なことに桁数指定ができません。

なので例えば、実数nを小数点以下2桁に丸めたい場合はMath.round(n * 100) / 100 などとしますよね。

毎回掛けたり割ったりするのも邪魔くさいので関数化すればヨロシイですねと、 実数 n を小数点以下 m 桁に四捨五入する関数は、以下のようになりますよっと(Nodeのモジュールとして書いています)。

桁数指定で小数点以下を四捨五入する関数(モジュール)

"use strict";
var assert = require("assert");

/**
 * 小数部の桁数指定で四捨五入。
 *
 * @param {number} n 元の数値
 * @param {number} m 桁数指定。0以上の整数。
 * @returns {number} 結果を返す
 */
function round(n, m) {
    assert(m == Math.round(m));
    assert(m >= 0);
    var r = Math.pow(10, m);
    return Math.round(n * r) / r;
}
module.exports = round;

ここで、 m に負の値を与えれば、整数部分も丸められるように一見思えるのですが、 m の絶対値が大きくなると桁落ちが発生して正しい結果が得られません。いつ桁落ちするかわからないので結局安心して使えません。

桁数指定で整数部分も四捨五入できる関数

てなことで、整数部も正しく四捨五入するために以下のようにしてみました。

"use strict";
var assert = require("assert");

/**
 * 桁数指定で四捨五入。
 *
 * @param {number} n 元の数値
 * @param {number} m 桁数指定。負の値で整数部を四捨五入
 * @returns {number} 結果を返す
 */
function round(n, m) {
    assert(m == Math.round(m));
    if(m < 0) {
        var i = Math.floor(n);
        var R = Math.pow(10, -m);
        var sgn = Math.sign(n);
        var h = sgn * R / 2;
        var mod = sgn * Math.abs(i % R);
        var up = sgn * (sgn >= 0 ? (mod >= h ? R : 0) : (mod < h ? R : 0));
        return  i - mod + up;
    }
    var r = Math.pow(10, m);
    return Math.round(n * r) / r;
}
module.exports = round;

当初「簡単!簡単!」と軽い気持ちで書いたら、 n が負の時に結果がおかしくて、最終的に結構ややこしくなってしまいました。 m < 0 の時、とか、 n の符号による判定が離散的でスッキリしないですねえ。なんかいい方法無いものかな。

言い訳

実は、あまり多くのテストケースを通していないので、ちょっと不安が残っております。

あと、npmには負の桁数指定ができるroundやceil, floorを提供するモジュールはあるようなのですが、 桁落ちに対してどうなっているのか明記されたものが、よくわかりませんでした(英語力の問題かも)。

「プロミス地獄」に落ちないための基本事項

「コールバック地獄」からボクらを救ってくれた「Promise」ですが、ふと気が付けば、ちょっと種類の違う別の地獄に落ちてる場合がありますよと。

「なんだPromiseお前もかっ!」的な(笑)

f:id:takamints:20170618182837p:plain

「コールバック地獄」は見た目にネストが深くて「ダメだコリャ感」がわかりやすい。 一方言わば「Promise地獄」は、パッと見スッキリしてるんだけど、少し複雑になると、ホントに正しく動いているのかどうか判別しにくい。 というのも、Promise的に間違っていてもJavaScriptの構文的には正しいことが多々あって、どこでバグっているかがわかりにくい。 結果、変なハマり方をしてしまうんですね。

てことで、今まで自分でハマった「Promise地獄」を思い返して知見をまとめておこうと思います。

約束 (創元推理文庫)
約束 (創元推理文庫)
posted with amazlet at 17.06.18
東京創元社 (2017-05-11)
売り上げランキング: 8,345

Promiseのハンドラーでtry~catchする必要はありません

Promiseオブジェクト生成時のexecutor関数内からエラーが投入されると、そのPromiseはrejectされます。

つまり特別な理由がない限りtry~catchでエラーを捕まえてrejectする必要はありません。

throw-error-from-ctor.js

function someAsync() {
    return new Promise(function(resolve, reject) {
        throw new Error("to reject the promise");
    });
}

someAsync().then(function() {
    console.error("OK.");
}).catch(function(err) {
    console.error("Error:", err.message);
});

実行結果

$ node throw-error-from-ctor.js
Error: to reject the promise

$ 

つまり以下は冗長です。実行結果は上と同じ。

throw-error-redundant.js

function someAsync() {
    return new Promise(function(resolve, reject) {
        try {
            throw new Error("to reject the promise.");
        } catch(err) {
            reject(err);
        }
    });
}

そしてコレはthenでも同じ

上のことはthenのonfulfilledにも当てはまります。つまりthenの中でtry~catchする必要はありません。 以下のPromiseもrejectされます。

throw-error-from-then.js

function someAsync() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() { resolve(); }, 1000);
    });
}

someAsync().then(function() {
    throw new Error("to reject the promise");
}).catch(function(err) {
    console.error("Error:", err.message);
});

しかしネストした非同期処理はtry-catchで囲むべきです

以下のコードでも、Promiseは結果的にrejectされていますが、投げられたエラーがキャッチされておらず、スタックトレースがプリントされています。

function someAsync() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() { resolve(); }, 1000);
    });
}

someAsync().then(function() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            throw new Error("to reject the promise");
        }, 1000);
    });
}).then(function() {
    console.error("OK.");
}).catch(function(err) {
    console.error("Error:", err.message);
});

実行結果

$ node throw-error-from-then-nest.js
throw-error-from-then-nest.js:10
            throw new Error("to reject the promise");
            ^

Error: to reject the promise
    at Timeout._onTimeout (throw-error-from-then-nest.js:10:19)
    at ontimeout (timers.js:365:14)
    at tryOnTimeout (timers.js:237:5)
    at Timer.listOnTimeout (timers.js:207:5)

実は予想外の動きでした。rejectされないと思っていたのです。もしかして古いバージョンではrejectされないかと確認してみましたが、少なくともnode 4.8.3ではrejectされていました。 なんにせよ、上のようにネストした非同期処理は、以下のようにtry~catchでエラーをハンドリングして明示的にrejectしたほうが良さそうです。

function someAsync() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() { resolve(); }, 1000);
    });
}

someAsync().then(function() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            try {
                throw new Error("to reject the promise");
            } catch(err) {
                reject(err);
            }
        }, 1000);
    });
}).then(function() {
    console.error("OK.");
}).catch(function(err) {
    console.error("Error:", err.message);
});

そのnew Promise、ホントに必要?

Promiseチェーンが長くなってくると、その一部分をまとめてPromiseを返す関数として独立させたくなったりしますが、その新しい関数の中でPromiseオブジェクトを生成する必要は多分ありません。

以下のようにthenがフラットにたくさん続くと、それはそれでイラッときます。

someAsync().then(function () {
    return asyncA1();
}).then(function () {
    return asyncA2();
}).then(function () {
    return asyncA3();
}).then(function () {
    return asyncB1();
}).then(function () {
    return asyncB2();
}).then(function () {
    console.log("OK.");
}).catch(function(err) {
    console.error("Error:", err.message);
});

非同期処理をAとBでまとめるには、以下のように組み直せば良いのですが、、、

function asyncA() {
    return asyncA1().then(function() {
        return asyncA2();
    }).then(function () {
        return asyncA3();
    });
}

function asyncB() {
    return asyncB1().then(function() {
        return asyncB2();
    });
}

someAsync().then(function () {
    return asyncA();
}).then(function () {
    return asyncB();
}).then(function () {
    console.log("OK.");
}).catch(function(err) {
    console.error("Error:", err.message);
});

しかしついつい、以下のように新しいPromiseを作って返すコードを書いてしまうことがあるんです。 間違ってはいないけれど冗長ですし、これが積み重なると、まさに「プロミス地獄」が始まる気がする。

function asyncA() {
    return new Promise(function(resolve, reject) {
        asyncA1().then(function() {
            return asyncA2();
        }).then(function () {
            return asyncA3();
        }).then(function() {
            resolve();    
        }).catch(function(err) {
            reject(err);
        });
    });
}

function asyncB() {
    return new Promise(function(resolve, reject) {
        asyncB1().then(function() {
            return asyncB2();
        }).then(function() {
            resolve();    
        }).catch(function(err) {
            reject(err);
        });
    });
}

someAsync().then(function () {
    return asyncA();
}).then(function () {
    return asyncB();
}).catch(function(err) {
    console.error("Error:", err.message);
});

まとめ

アレっ?書き始める前はもっとあった気がするんですけどね。

思いついたら随時追記するつもりですー。