メソッドチェーンのパターンを考えた
おそらく、私はただpipeline operatorが欲しいだけだろう。以下のようなコードを本当に嫌っている。
const x1 = f(x0);
const x2 = g(x1);
const x3 = h(x2);
Promiseの本懐はこれを解決することではないが、メソッドチェーンでpipeline operatorのようなことができるので、好きだ。もともとRubyのObject#tapも好きだったし。
await Promise.resolve(x0).then((x) => {
return Promise.resolve(f(x));
}).then((x) => {
return Promise.resolve(g(x));
}).then((x) => {
return Promise.resolve(h(x));
});
シンタックスはいかついが、変数がクロージャに閉じていて気持ちがいい。こういうものを欲しいと思った時にサクッと対応したいけど、毎回考えてしまうので、今回は一般的に考えた。
RubyのObject#tapの再実装
Object.prototype.tapにブチ込めばよい。
Object.prototype.tap = function (callback) {
return callback(this);
};
簡単すぎるし、モンキーパッチなので、これを避けていきたい。
基礎パターン (Promise風)
function createThenable(object, callback) {
const _object = callback(object);
return {
value: _object,
then: function (_callback) {
return createThenable(_object, _callback);
},
await: function () {
return _object;
},
};
}
awaitは、操作感が伝わりやすいかと思い、この命名にした。valueをプロパティで呼ぶだけでもよい。上のコードは、このように使う。
createThenable(1, (i) => {
return i + 1;
}).then((i) => {
return i + 1;
}).then((i) => {
return i + 1;
}).await();
Object#tapが好きだと書いたが、これを再実装するには、createThenableをcreateTappableに、thenをtapに書き換えればそれで済むだろう。よく考えれば、前述したPromiseの副産物はObject#tapと同じものだった。
Array.prototype.pushでメソッドチェーンしたい
そういえば、RubyのArray#pushは返り値がselfなのでメソッドチェーンできるけど、JavaScriptのArrayはそうなっていない。RubyからJavaScriptへ乗り換えた者には覚えがあるだろう。
createThenable([], (array) => {
array.push(1);
return array;
}).then((array) => {
array.push(2);
return array;
}).then((array) => {
array.push(3);
return array;
}).await();
さきほどのコードをそのまま利用するとこうなるが、明らかに冗長なので特化させたい。
function pushArray(array, callback) {
array.push(callback(array));
return {
push: function(_callback) {
return pushArray(array, _callback);
},
array: array,
};
}
すると、以下のように書くことができる。
pushArray([], () => {
return 1;
}).push(() => {
return 2;
}).push(() => {
return 3;
}).array;
手軽なルーターを書きたい
ルーターというのは、Web Application Frameworkのルーターのことで、正規表現にマッチしたら引数のクロージャを実行する、というのを繰り返すものが欲しい。RegExp.prototype.execの返り値がtruthyの場合に、クロージャに引数として渡したいし、それをマッチするまで繰り返したい。この記事を書くきっかけとなった問題意識がこれ。
function router(routes, regexp, handler) {
routes.push({ regexp: regexp, handler: handler });
return {
bind: function (_regexp, _handler) {
return router(routes, _regexp, _handler);
},
route: function (string) {
for (let i = 0; i < routes.length; ++i) {
const r = routes[i];
const match = r.regexp.exec(string);
if (match) {
return r.handler(match);
}
}
},
};
}
この様に使う。
router([], /aaa/, (match) => {
return console.log(1);
}).bind(/bbb/, (match) => {
return console.log(2);
}).route('bbbbbb');
先頭の空配列リテラルが美しくないが、再帰のために必要なものなので、どうしても隠したければこれを隠蔽する関数を書くとよいだろう。ところで、私がルーターと呼んだ「正規表現にマッチしたらクロージャを実行し、マッチしなければ次の候補を試す」ものは頻出だと思うが、通常はどのように書くのだろうか。