銀の弾丸

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

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

f:id:takamints:20171015205935j:plain

octaveを使って線形回帰で「アヤメの分類」をやってみました。

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

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

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

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

octaveWindowsへの導入は、以下の記事を参考に。

takamints.hatenablog.jp

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

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

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

機械学習のためのオープンな訓練データとして、アヤメ(iris)というのがあるらしいと、この度初耳。

データセットは150件と小さくて、4つのパラメータで3種類のアヤメを同定するというものです。

上記のファイルをダウンロードするため以下のシェルスクリプトを書きました。

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

iris.dataをダウンロードしてCSVを変換しています(変換については次項↓参照)。

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 に出力してます(おっと、エラーを一切見ていませんが良い子は絶対マネしないでw)。

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"));
    });
});

データについて

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.  ```

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

以下のoctave/matlabスクリプトが今回の御本尊。

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

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

iris.m

#
# Classification of iris using linear regression
#

# Clear all mat
clear

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

#
# Select training dataset
#
Dt = D(1:size(D, 1), :);    # Dt = D(1:2*size(D, 1)/3, :);
Xt = Dt(:, 1:(size(Dt,2)-1));    # Input
Xt = [ones(size(Xt,1),1) Xt];
Yt = Dt(:, size(Dt,2));      # Result

#
# Select validation dataset
#
Dv = D(1:size(D, 1), :);    # Dv = D(2*size(D, 1)/3+1:size(D,1), :);
Xv = Dv(:, 1:(size(Dv,2)-1));
Xv = [ones(size(Xv,1),1) Xv];
Yv = Dv(:, size(Dv,2));

training_data_count = size(Xt,1);
learning_rate = 0.00001     # learning rate
iteration = 5000000
report_interval = round(iteration / 10);

#
# Training
#
theta = ones(1, size(Xt,2)); # factor
numOfParam = size(theta, 2);
update = [1, numOfParam];
for n = 1:iteration
    diff = Xt * theta' - Yt;
    for j = 1:numOfParam
        update(1,j) = diff' * Xt(:,j);
    endfor
    theta = theta - learning_rate * update / training_data_count;
    if mod(n,report_interval) == 0
        cost = (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 = diff' * diff / ( 2 * size(Xv,1) )

結果検証

以下が上記スクリプトの実行結果。 150件中3件の判定間違いがありますが、これが限界みたいです。

learning_rate = 1.0000e-005
iteration =  5000000
cost =  0.026944
cost =  0.025440
cost =  0.024725
cost =  0.024366
cost =  0.024169
cost =  0.024048
cost =  0.023963
cost =  0.023897
cost =  0.023841
cost =  0.023792
error_index =  71
error_index =  84
error_index =  134
theta =

   0.586249  -0.177805  -0.067764   0.244015   0.621859

validationErrorRate =  0.020000
validationTotalCost =  0.023792

(2017-10-21追記①)

データの可視化をしようとしていて、単なる偶然ですが学習回数を少なくできるのを見つけました。 上の実行例では学習を500万回繰り返していますが、1万回で同じ結果を得られます。 learning_rateは1000倍で、学習は数秒で終わります。さらに誤差が若干少ない。

iris
learning_rate =  0.010000
iteration =  10000
cost =  0.025441
cost =  0.024366
cost =  0.024048
cost =  0.023897
cost =  0.023792
cost =  0.023708
cost =  0.023636
cost =  0.023575
cost =  0.023522
cost =  0.023476
error_index =  71
error_index =  84
error_index =  134
theta =

   0.464549  -0.152014  -0.066062   0.234802   0.621481

validationErrorRate =  0.020000
validationTotalCost =  0.023476

(2017-10-21追記①)おわり

別途「Iris-setosa」を、他の2つから完璧に(そしてかなり簡単に)分離できました。 しかし「Iris-versicolor」と「Iris-virginica」はパラメータ平面において数件が領域を共有してるため、線形モデルで分離するのは困難なようです。

これ以上判定精度を挙げるには別の新たなパラメータが必要になるはずです。 そんなこんなで、この分類は成功していると言えそうです。 当初はなんかおかしいと思っていましたが(笑)

結果が離散的な分類問題なので、ロジスティック回帰で解く必要があるのかも?と思いましたが、線形モデルである限りは結果は変わらないはずです。

また、ディープラーニングではキレイに分離できるはずですが、しかし、おそらくそれはオーバーフィッティングであって、一般的な解とはいえない(つまり別のデータセットを持ってきたら、やはり間違う)はず。そして、学習には膨大な中間層が必要で時間もかかると思います。

(2017-10-21追記②)

入力データと結果の可視化

入力データと判定エラーのチャートを描いてみました。 (赤:Setona、緑:Versicolour、青:Virginica)

f:id:takamints:20171021124312p:plain

○が入力データで、●は判定ミスです(間違ってこの色が示す種類に判定された)。

ご覧のように、緑:Versicolour、青:Virginicaとが入り混じっている箇所があります。 この部分がリニアには分離できないと書かれているところだと思います。

ちなみにチャートを描くスクリプトは、以下になります。

# Draw iris charts
[];

function show_chart(D, error_data, featureX, featureY, chartTitle)

    hold all

    Dy0 = D( D( :, 5 ) == 0, : );
    scatter(Dy0(:,featureX), Dy0(:,featureY), 'r')

    Dy1 = D( D( :, 5 ) == 1, : );
    scatter(Dy1(:,featureX), Dy1(:,featureY), 'g')

    Dy2 = D( D( :, 5 ) == 2, : );
    scatter(Dy2(:,featureX), Dy2(:,featureY), 'b')

    error_data_r = error_data(error_data(:,5)==0, :);
    scatter(error_data_r(:, featureX), error_data_r(:, featureY), [], 'r', 'filled')

    error_data_g = error_data(error_data(:,5)==1, :);
    scatter(error_data_g(:, featureX), error_data_g(:, featureY), [], 'g', 'filled')

    error_data_b = error_data(error_data(:,5)==2, :);
    scatter(error_data_b(:, featureX), error_data_b(:, featureY), [], 'b', 'filled')

    title(chartTitle)

endfunction

subplot(1,2,1)
show_chart(D, error_data, 1, 2, 'X:Sepal length, Y:Sepal width')

subplot(1,2,2)
show_chart(D, error_data, 3, 4, 'X:Petal length, Y:Petal width')

(2017-10-21追記②)おわり

公開リポジトリ

上記のスクリプトは、以下のGitリポジトリに置いてます。 (最新版では学習回数は1万回に設定されています)。 試してみたい人は是非どうぞ。

github.com

データ解析のためのロジスティック回帰モデル
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);
});

まとめ

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

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