銀の弾丸

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

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

まとめ

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

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

WHATWG Fullscreen API を仕様通りに使えるモジュール「fullscrn」

f:id:takamints:20170528140557p:plain

WEBページ内の特定HTML要素を画面全体に広げられるフルスクリーンAPIのラッパーモジュールをnpmで公開しました。

WHATWGが策定しているフルスクリーンAPIは、現状(2017年5月現在)、多くのブラウザで、プリフィックス付きの実装(mozとかwebkitというアレですね)となっています。

なので、いろんなブラウザで動作させるには結構邪魔くさいことをするわけですが、このモジュールを使えば標準仕様と同じように利用できます。

目次

  1. リリースファイル
  2. サンプル
  3. API
    1. プロパティ
    2. メソッド
    3. イベント
  4. リンク
  5. あとがき

↓npm fullscrnはコチラです。

www.npmjs.com

HTML5 & CSS3 デザインレシピ集
狩野 祐東
技術評論社
売り上げランキング: 3,429
アッと驚く為五郎
アッと驚く為五郎
posted with amazlet at 17.05.28
EMIミュージック・ジャパン (2015-09-23)
売り上げランキング: 247,047

1. リリースファイル

  • fullscrn.js - SCRIPTタグで読み込む用。Browserifyでまとめているからちょっと読みにくいかも。
  • fullscrn.min.js - ↑をミニファイしたもの(uglify使用)
  • index.js - ソースファイル。npmでBrowserifyを使用してrequireする場合はコチラが使用されると思います。

2. サンプル

あまり意味のないサンプルですけどすみません。動きはわかると思います。

sample/injected.html

<body onload="main();">
    <button type="button" onclick="request1();"
    >Full&gt;&gt;</button>
    <span id="panel">
        <button type="button" onclick="request2();"
        >Full&gt;&gt;</button>
        <button type="button" id="exitButton"
        onclick="exit();">&lt;&lt;Exit</button>
    </span>
    <script src="fullscrn.js"></script> 
    <script>
        var panel = document.getElementById("panel");
        var exitButton = document.getElementById("exitButton");
        Fullscreen.debugMode(true);// Enables debug log
        function main() {
            // Handle change event 
            document.addEventListener("fullscreenchange",
                function() {
                    var fse = document.fullscreenElement;
                    console.log("FULLSCREEN CHANGE: " +
                        ((fse == null)? "(null)": "#" + fse.id));
                });
 
            // Handle error event 
            document.addEventListener("fullscreenerror",
                function() { console.log("FULLSCREEN ERROR"); });
 
            request1(); // This should be error 
        }
        function request1() {
            panel.requestFullscreen().then(function(){
                console.log("request1 done.");
            }).catch(function(err) {
                console.error(err.message);
            });
        }
        function request2() {
            exitButton.requestFullscreen().then(function(){
                console.log("request2 done.");
            }).catch(function(err) {
                console.error(err.message);
            });
        }
        function exit() {
            document.exitFullscreen()
            .then(function(){
                console.log("exit done.");
            }).catch(function(err) {
                console.error(err.message);
            });
        }
    </script> 
</body>

3. API

以降の各インターフェースは、標準仕様の通りに使用できるように、DOMの DocumentクラスやElementクラスのprototypeに、インジェクトしています。

ただし、完全一致を狙っているわけではないので、完全に同じ挙動をすることを保証しません。各社ブラウザ間でも挙動が微妙に違っていたりしますが、そこには全く触れていません。

また、2017-05-16にWHATWGの仕様が改定されており、本モジュール作成にあたっては、それ以前の仕様を参考にしていたため、違うところがあるかもしれません(未確認)。

※ 別途、独立したAPIもエクスポートしていますが、使う必要が無いのでここでは説明しません。

1) プロパティ

Document.fullscreenEnabled

フルスクリーンAPIが使用できるかどうかを示すbool型のプロパティ。スクリプトの読み込み時に決定します。

Document.fullscreenElement

全画面モードになっている要素を保持します。全画面モードでないときはnullです。

Document.fullscreen

参照時点で全画面モードかどうかを示すbool型のプロパティ(Document.fullscreenElement != null と同等)。

2) メソッド

以下、どちらも、操作の完了時に解決するPromiseを返します。まあ殆どの場合無視して構いません。

Element.requestFullscreen()

HTML要素で全画面モードを開始する要求を行います。

Document.exitFullscreen()

全画面モードを終了します。

3) イベント

全画面の状態が変化した場合や、エラー発生時のイベントです。

どちらも、document.addEventListenerイベントハンドラを登録して利用してください。 (Document.onfullscreenchange / Documentonfullscreenerror には未対応です)

Document "fullscreenchange"

全画面モードの状態変化時に発生します。

Document "fullscreenerror"

全画面モードに関するエラーがあった時に発生します。

ていうか標準仕様と同じですから。

4. リンク

5. あとがき

標準仕様と実装の混乱ぶり

現状、フルスクリーンAPIはプリフィックス付きで実装されているのに、既にObsoleteな仕様があったりしてカオスです。

さらに「Fullscreenなの?FullScreenなの?どっち?」という若干低レベルな混乱ぶりも垣間見れ、非常に使いづらい状況でした。

各社勝手に実装して、後から仕様をまとめるからこうなるんだろうなあ。知らんけど。

どうでもいいこと

このモジュール、ほとんどフルスクリーンAPIのPolyfillだと言えると思うのですが、npmでは既にfullscreenというモジュールが公開されていたため、仕方なくfullscrnという名になってます。

アチラはAPIのインターフェースがWHATWGの仕様に合致していないっぽいので、色んな意味でちょっと残念。 (# ̄З ̄)

しかしダウンロード数が遥かに多くて(約20倍!)、本家といえばアチラなのかも?

SVGの重なり順序をJavaScriptで制御する「svg-z-order」

f:id:takamints:20170423172653p:plain

JavaScriptからSVG要素の重なり順(Z-Order)を操作するモジュールのご紹介。

SVG要素には、HTMLに使えるz-indexスタイルが効きません。 なので、重なり順を変更するには、単純に要素を並べ替える必要があります。

コードからDOM要素を並べ替えるにはNodeクラスinsertBeforeメソッドが使えますNodeクラスはSVG要素SVGElementの基本クラスですから、SVG要素でも使えます。

SVG関連最新記事:
takamints.hatenablog.jp takamints.hatenablog.jp

ところで、SVG要素にはz-indexが効かないことを「SVGの致命的な欠陥」と言ってる人がいますが、さすがにそれは言い過ぎですよ。SVGは単体で存在できる画像ファイルであってHTMLとの直接的なつながりはありません。単体のSVGファイル中で図形の重なり順序をz-indexで定義されていると、画像としてどのように表示されるか理解しにくいはずです。要素の順序で重なり順序が決定される方が、SVG的に「よりシンプル」なんですね。

さらに「HTMLのDIVにSVGを入れてz-indexで…」などというヤヤコシイ方法が検索で出てくるのですが、これは既にSVGではなくなっていますから、一般的な解決法とはいえないように思います。全体のスケールを変えるとかViewportを移動したいとかいったシーンで困りそう。

というか、DOM要素の並べ替え自体は前述のようにinsertBeforeで簡単なんです。 DOMのベタな扱いに慣れていないと難しそうに思うのかもしれないですね。

ただし、ベタにやると「どれの前に持ってくる?」といったコードが煩雑になるのは確かです。 てことで怒りに任せて(?)npmを書きなぐりました。以降、このモジュールのサンプルとかAPIを説明します。

www.npmjs.com

とりあえずは動くサンプル

以下は、このモジュールを使ったサンプルです。コードは下の方に掲載してます。 3つの円をクリックすると、その要素にドロップダウンで選ばれているメソッドを適用します。

使い方

このモジュールを使って、特定要素を最前面へ持ってくるコードを少し冗長に書いてみました。

//モジュール取得
var svgz = require("svg-z-order");// (1)

//SVG要素 g#foo を最前面へ表示
var g = svg.getElementById("foo");
var svgzG = svgz.element(g); //(2)
svgzG.toTop(); //(3)
 
// D3.jsで使う(4)
var d3 = require("d3");
var d3g = d3.select("#foo");
svgz.element(d3g.node()).toTop();
  1. モジュールをインポートして、
  2. element メソッドで、DOM要素を参照するインスタンスを作り
  3. 最前面へ表示します。
  4. D3.jsを使っていますが、例として書いているだけで、依存関係はありません。

上の例では、Browserifyを使って、 var svgz = require("svg-z-orger");でモジュールを取り込んでいますが、 HTMLで<script src="svg-z-order.js"/>としている場合は、 (2)の行の svgz.element(g) を、 svgz_element(g) に変更して下さい。 requireできない環境ではグローバルスコープに定義します。

API

element(e:Element)

SVG要素を参照する SVGZElement クラスのインスタンスを返します。 Z-Orderを操作するAPIは SVGZElementクラスのメソッドです。

パラメータ

  • e:Element - 参照するDOM要素を指定します。

SVGZElementクラス

SVGZElement.toTop()

参照している要素を最前面に移動。

SVGZElement.toBottom()

参照している要素を最背面に移動。

SVGZElement.moveUp(e:Element / n:number)

参照している要素を上(前面)へ移動。

パラメータ

  • e:Element - この要素よりも上になるように移動します。
  • n:number - 要素の並びの中でこの回数分だけ上へ移動します。

要素を指定したときはその要素よりも上へ。数値指定した場合は、その回数だけ上へ移動。

SVGZElement.moveDown(e:Element / n:number)

参照している要素を下(背面)へ移動。

パラメータ

  • e:Element - この要素よりも下になるように移動します。
  • n:number - 要素の並びの中でこの回数分だけ下へ移動します。

要素を指定したときはその要素よりも下へ。数値指定の場合は、その回数だけ下へ移動。

サンプルコード

一番上にあるサンプルのコードです。

/sample/web/index.js

リポジトリ

www.npmjs.com github.com

いくつになってもお勉強

JavaScriptのプログラムからSVG要素を最前面へもってくる方法を検索したら、結構微妙な情報が出てきたので少しインターネッツに失望しました。

曰く、

  1. SVG単体では不可能なので、HTMLのDIVにSVGを書いて重ね合わせて、DIVのz-indexで制御するしかありません。
  2. 対象のSVG要素をディープコピーして appendChild 。元要素は removeChild。

こういう情報を見て、当初「エラいヤヤコシイな」と思いましたが、どうにも納得がいかなくて、いろいろやってみているとDOMの標準機能で可能だと判明。 当初「複製作って、追加して、元のを消す」という処理手順をイメージしていましたが、移動できますね。 Node.appendChildNode.insertBeforeは、既にDOMツリーにある要素は移動するのですが、メソッド名を見る限りではそう思えないところが落とし穴かも。

いくつになってもお勉強です。

Node.js / npm 関連記事

takamints.hatenablog.jp takamints.hatenablog.jp takamints.hatenablog.jp takamints.hatenablog.jp takamints.hatenablog.jp