JavaScript

メソッドチェーンのパターンを考えた

メソッドチェーンのパターンを考えた

おそらく、私はただ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');

先頭の空配列リテラルが美しくないが、再帰のために必要なものなので、どうしても隠したければこれを隠蔽する関数を書くとよいだろう。ところで、私がルーターと呼んだ「正規表現にマッチしたらクロージャを実行し、マッチしなければ次の候補を試す」ものは頻出だと思うが、通常はどのように書くのだろうか。