Skip to content

Latest commit

 

History

History
263 lines (198 loc) · 12.1 KB

promise-chain.adoc

File metadata and controls

263 lines (198 loc) · 12.1 KB

Promiseとメソッドチェーン

Promiseは thencatch 等のメソッドを繋げて書いていきます。 これはDOMやjQuery等でよくみられるメソッドチェーンとよく似ています。

一般的なメソッドチェーンは this を返すことで、メソッドを繋げて書けるようになっています。

Note

メソッドチェーンの作り方については メソッドチェーンの作り方 - あと味 などを参照するといいでしょう。

一方、Promiseは毎回新しいpromiseオブジェクトを返すようになっていますが、 一般的なメソッドチェーンと見た目は全く同じです。

このセクションでは、一般的なメソッドチェーンで書かれたものを インターフェースはそのままで内部的にはPromiseで処理されるようにする方法について学んでいきたいと思います。

fsのメソッドチェーン

以下のような Node.jsのfsモジュールを例にしてみたいと思います。

また、今回の例は見た目のわかりやすさを重視しているため、 現実的にはあまり有用なケースとはいえないかもしれません。

fs-method-chain.js
link:src/promise-chain/fs-method-chain.js[role=include]

このモジュールは以下のようにread → transform → writeという流れを メソッドチェーンで表現することができます。

const File = require("./fs-method-chain");
const inputFilePath = "input.txt";
const outputFilePath = "output.txt";
File.read(inputFilePath)
    .transform((content) => {
        return ">>" + content;
    })
    .write(outputFilePath);

transform は引数で受け取った値を変更する関数を渡して処理するメソッドです。 この場合は、readで読み込んだ内容の先頭に >> という文字列を追加しているだけです。

Promiseによるfsのメソッドチェーン

次に先ほどのメソッドチェーンをインターフェースはそのまま維持して 内部的にPromiseを使った処理にしてみたいと思います。

fs-promise-chain.js
link:src/promise-chain/fs-promise-chain.js[role=include]

内部に持ってるpromiseオブジェクトに対するエイリアスとして thencatch を持たせていますが、それ以外のインターフェースは全く同じ使い方となっています。

そのため、先ほどのコードで require するモジュールを変更しただけで動作します。

const File = require("./fs-promise-chain");
const inputFilePath = "input.txt";
const outputFilePath = "output.txt";
File.read(inputFilePath)
    .transform((content) => {
        return ">>" + content;
    })
    .write(outputFilePath);

File.prototype.then というメソッドは、 this.promise.then が返す新しいpromiseオブジェクトを this.promise に対して代入しています。

これはどういうことなのかというと、以下のように擬似的に展開してみると分かりやすいでしょう。

const File = require("./fs-promise-chain");
File.read(inputFilePath)
    .transform((content) => {
        return ">>" + content;
    })
    .write(outputFilePath);
// => 擬似的に以下のような流れに展開できる
promise.then(() => {
    return fs.readFileSync(filePath, "utf-8");
}).then((content) => {
    return ">>" + content;
}).then(() => {
    return fs.writeFileSync(filePath, data);
});

promise = promise.then(…​) という書き方は一見すると、上書きしているようにみえるため、 それまでのpromiseのchainが途切れてしまうと思うかもしれません。

イメージとしては promise = addPromiseChain(promise, fn); のような感じになっていて、 既存のpromiseオブジェクトに対して新たな処理を追加したpromiseオブジェクトを作って返すため、 自分で逐次的に処理する機構を実装しなくても問題ないわけです。

両者の違い

同期と非同期

fs-method-chain.jsPromise版の違いを見ていくと、 そもそも両者には同期的、非同期的という大きな違いがあります。

fs-method-chain.js のようなメソッドチェーンでもキュー等の処理を実装すれば、 非同期的なほぼ同様のメソッドチェーンを実装できますが、複雑になるため今回は単純な同期的なメソッドチェーンにしました。

Promise版はコラム: Promiseは常に非同期?で紹介したように 常に非同期処理となるため、promiseを使ったメソッドチェーンも非同期となっています。

エラーハンドリング

fs-method-chain.jsにはエラーハンドリングの処理は入っていないですが、 同期処理であるため全体を try-catch で囲むことで行えます。

Promise版 では内部で利用するpromiseオブジェクトの thencatch へのエイリアスを用意してあるため、通常のpromiseと同じように catch によってエラーハンドリングが行えます。

fs-promise-chainでのエラーハンドリング
const File = require("./fs-promise-chain");
File.read(inputFilePath)
    .transform((content) => {
        return ">>" + content;
    })
    .write(outputFilePath)
    .catch((error) => {
        console.error(error);
    });

fs-method-chain.jsに非同期処理を加えたものを自力で実装する場合、 エラーハンドリングが大きな問題となるため、非同期処理にしたい時は Promiseを使うと比較的簡単に実装できるといえるかもしれません。

Promise以外での非同期処理

このメソッドチェーンと非同期処理を見てNode.jsに慣れている方は Stream が思い浮かぶと思います。

Stream を使うと、 this.lastValue のような値を保持する必要がなくなることや大きなファイルの扱いが改善されます。 また、Promiseを使った例に比べるとより高速に処理できる可能性が高いと思います。

streamによるread→transform→write
readableStream.pipe(transformStream).pipe(writableStream);

そのため、非同期処理には常にPromiseが最適という訳ではなく、 目的と状況にあった実装をしていくことを考えていくべきでしょう。

Note
Node.jsのStreamはEventをベースにしている技術

Node.jsのStreamについて詳しくは以下を参照して下さい。

Promiseラッパー

話を戻してfs-method-chain.jsPromise版の両者を比べると、 内部的にもかなり似ていて、同期版のものがそのまま非同期版でも使えるような気がします。

JavaScriptでは動的にメソッドを定義することもできるため、 自動的にPromise版を生成できないかということを考えると思います。 (もちろん静的に定義する方が扱いやすいですが)

そのような仕組みはES Promisesにはありませんが、 著名なサードパーティのPromise実装である bluebird などには Promisification という機能が用意されています。 また、Node.jsのコアモジュールであるutilモジュールには、 util.promisify というAPIが用意されています。

これを利用すると以下のように、その場でPromise版のメソッドを作成して利用できるようになります。

const fs = require("fs");
const util = require("util");
// コールバック版のAPIからPromise版を作成する
const readFile = util.promisify(fs.readFile);

readFile("myfile.js", "utf8").then((contents) => {
    console.log(contents);
}).catch((e) => {
    console.error(e.stack);
});

ArrayのPromiseラッパー

先ほどのutil.promisifyが何をやっているのか少しイメージしにくいので、 次のようなネイティブ Array のPromise版となるメソッドを動的に定義する例を考えてみましょう。

JavaScriptにはネイティブにもDOMやString等メソッドチェーンが行える機能が多くあります。 Array もその一つで、mapfilter 等のメソッドは配列を返すため、メソッドチェーンが利用しやすい機能です

array-promise-chain.js
link:src/promise-chain/array-promise-chain.js[role=include]

ネイティブのArrayと ArrayAsPromise を使った場合の違いは 上記のコードのテストを見てみるのが分かりやすいでしょう。

array-promise-chain-test.js
link:test/array-promise-chain-test.js[role=include]

ArrayAsPromise でもArrayのメソッドを利用できているのが分かります。 先ほどと同じように、ネイティブのArrayは同期処理で、ArrayAsPromise は非同期処理という違いがあります。

ArrayAsPromise の実装を見て気づくと思いますが、Array.prototype のメソッドを全て実装しています。 しかし、array.indexOf など Array.prototype には配列を返さないものもあるため、全てをメソッドチェーンにするのは不自然なケースがあると思います。

ここで大事なのが、同じ値を受けるインターフェースを持っているAPIはこのような手段でPromise版のAPIを自動的に作成できるという点です。 このようなAPIの規則性を意識してみるとまた違った使い方が見つかるかもしれません。

Note

先ほどの util.promisify は、 Node.jsのCoreモジュールの非同期処理には function(error, result){} というように第一引数に error が来るというルール(エラーファーストコールバック)を利用して、自動的にPromiseでラップしたメソッドを生成しています。

また、Node.js 10からは fs モジュールに Promise版のAPI が追加されています。

まとめ

このセクションでは以下のことについて学びました。

  • Promise版のメソッドチェーンの実装

  • Promiseが常に非同期の最善の手段ではない

  • Promisification

  • 統一的なインターフェースの再利用

ES PromisesはCoreとなる機能しか用意されていません。 そのため、自分でPromiseを使った既存の機能のラッパー的な実装をすることがあるかもしれません。

しかし、何度もコールバックを呼ぶEventのような処理がPromiseには不向きなように、 Promiseが常に最適な非同期処理という訳ではありません。

その機能にPromiseを使うのが最適なのかを考えることはこの書籍の目的でもあるため、 何でもPromiseにするというわけではなく、その目的にPromiseが合うのかどうかを考えてみるのもいいと思います。