lodash-fpで関数型プログラミングに歩み寄る

  • 5
    Like
  • 0
    Comment

本記事はアイスタイルAdvent Calendar 2016の21日目の記事です。
ポイントシステムなど弊社の基盤系API開発を担当しているJavaエンジニアが担当いたします。

社内ではJavaばかり書いていますが好きなのはフロントエンド開発で、趣味でたまーにJavaScriptを書いています。

本記事ではlodash.jsの派生モジュールであるlodash-fpを、定時後の体力が続く限りご紹介します。

lodash-fpとは

lodash.jsと言えばUnderscore.jsと並ぶJavaScriptの定番Utilityライブラリです。ネイティブにサポートされるメソッドが充実してきたとはいえ、まだまだお世話になっている方は多いかと思います。

lodash-fpはlodash.jsのメソッドをより関数型らしいスタイルで提供するモジュールです。カリー化済みのメソッドや、lodash.jsのメソッドに対するより関数型らしい名称のエイリアスなどが提供されています。

単体で利用しても便利ですが、FRPライブラリや非同期通信を行うライブラリなどCallback関数・Iteratorを多様するライブラリを補助するのに大変都合のよいモジュールです。

lodash-fpの「関数型らしい」特徴についてlodash.jsと異なる点は何か、何がうれしいのかを定時後の体力が尽きるまでご紹介します。

カリー化済みのメソッド

どう違う?

lodash-fpのメソッドは多くがデフォルトでカリー化されています。

カリー化の厳密な定義は他の方の記事に任せるとして、一言で言い表すと「複数の引数を持つ関数から、最初の引数を固定し、固定されていない残りの引数を取る関数を生成すること」になるかと思います。
噛み砕いて説明したつもりですが、情報がねじ曲がって伝わっていないか大変不安です。

実際にカリー化されたlodash-fpのmapメソッドを利用した例を見てみましょう。

// 引数で指定されたオブジェクトの配列からcompanyプロパティの値だけを抽出する

const companyData = [
  {company: 'istyle', office: '東京都港区赤坂一丁目'},
  {company: 'コスメ・コム', office: '東京都港区赤坂一丁目'},
  {company: 'コスメネクスト', office: '東京都港区赤坂一丁目'}
];

// 通常の `map` 
_.map(companyData, 'company');
// → ["istyle", "コスメ・コム", "コスメネクスト"]

// lodahs-fpのカリー化された`map` 
fp.map('company')(companyData);
// → ["istyle", "コスメ・コム", "コスメネクスト"]

lodash-fpのmapを利用する例ではメソッド呼び出しが二回行われています。

順を追って解説すると下記のことを行っています。
1. 「引数に渡されたオブジェクトからcompanyプロパティの値を抽出するFunctionオブジェクト」を生成
2. 1で生成されたこのFunctionオブジェクトに処理対象となるオブジェクトを渡して実行

lodash-fpは提供するメソッドに対してカリー化をあらかじめ行っているだけで、カリー化が自体はlodash.jsでもcurryメソッドで可能となっています。

何がうれしい?

curryメソッドを利用しなくともカリー化されているため、lodash.jsのメソッドを基にしたFunctionオブジェクトを簡単に生成できます。

生成されたFunctionオブジェクトはArrayオブジェクトのforEach()メソッドや、非同期処理のcallback Function、FRPライブラリでのiteratorとして使用できます。

せっかくなのでカリー化そのもののメリットについても例を挙げます。

下記趣味で作ったnode製botプログラムの一部です。実装当初、Promiseのexecutor関数のおかげで少しだけネストが深くなっており、少し注視して読まなければならない実装になっていました。

/**
 * 記事更新告知用アカウントを用いて任意のメッセージをつぶやきます
 *
 * @param message つぶやく文字列
 * @private
 * @return {Promise.<Object>} TwitterAPIから返却されたJSONオブジェクト
 * */
function notify (message) {
 return new Promise((resolve, reject) => {
    client.post('statuses/update', {status: message}, (error, message, response) => {
      if(error){
        reject(response);
        return;
      }
      resolve(response);
    });
  });
}

これをcurryメソッドを使ってリファクタリングしたのがこちらです。

/**
 * 記事更新告知用アカウントを用いて任意のメッセージをつぶやきます
 *
 * @param message つぶやく文字列
 * @private
 * @return {Promise.<Object>} TwitterAPIから返却されたJSONオブジェクト
 * */
function notify (message) {
  return new Promise((resolve, reject) =>{
    const _callback = _.curry(callback)(resolve)(reject);
    client.post('statuses/update', {status: message}, _callback);
  });
}

/**
 * TwitterAPI 'statuses/update' Callback
 *
 * @private
 * */
function callback(resolve, reject, error, message, response) {
  if (error) {
    reject(response);
    return;
  }
  resolve(response);
};

Promiseのexecutor関数が持つべき引数と、postメソッドのCallback関数が持つべき引数の両方を取る汎用的なメソッドを事前に定義しています。

非同期処理の実行直前にカリー化を行い、このnotifyメソッドに特化したFunctionオブジェクトを生成することで、executor関数内でアロー関数式を用いる必要がなくなりネストが解消しました。

このリファクタリング自体がカリー化で解決すべきものなのかは微妙なところですが、多くの引数を取る汎用的なメソッドから特定の目的に特化したメソッドを生成しているという点に注目してください。

lodash-fpのメソッドはこのカリー化がデフォルトで行われているため、より手軽にそのメリット受けることができるようになっています。

iterateeから始まりdataで終わる引数順序

どう違う?

大抵のlodash.jsのメソッドはmethod(処理対象のdata, 処理を行うiteratee)という順で引数が定義されています。これに対しlodash-fpのメソッドはmethod(処理を行うiteratee, 処理対象のdata)といった順で引数が定義されています。

例えば先のmapメソッドの例ですが、あえてカリー化を活用せずに関数呼び出しを行うと下記の様に引数の順番が逆転します。

// 引数で指定されたオブジェクトの配列からcompanyプロパティの値だけを抽出する

const companyData = [
  {company: 'istyle', office: '東京都港区赤坂一丁目'},
  {company: 'コスメ・コム', office: '東京都港区赤坂一丁目'},
  {company: 'コスメネクスト', office: '東京都港区赤坂一丁目'}
];

// lodashの `map` 
_.map(companyData, 'company');
// ➜ ["istyle", "コスメ・コム", "コスメネクスト"]

// lodahs-fpの`map` 
fp.map('company', companyData);
// ➜ ["istyle", "コスメ・コム", "コスメネクスト"]

mapメソッドは引数が二つですが、三つ以上の引数を持つ関数も引数順序が変化します。引数が多いメソッドは直感で予想しにくい順になるものもありますので、使用前に公式ドキュメントの記述に目を通すことをお勧めします。

何がうれしい?

カリー化は第一引数から行われます。処理対象となるdataより処理内容を表すiterateeが先に並ぶことで使い勝手の良いFunctionオブジェクトを生成できます。

mapメソッドの例で仮に引数の順序がそのままだったとすれば、生成されるFunctionオブジェクトは「予め決められたオブジェクトの配列に、引数で受け取ったIterateeを適用するFunctionオブジェクト」になります。使い道が無いとは思いませんが、Callback関数などには活用できそうにありません。

ちなみにlodash.jsにも共通する話ですが、カリー化で第一引数以降の引数から固定したいという場合公式ドキュメントの例にある通りlodash.js/lodash-fpを参照する変数をプレースホルダとすることで実現できます。

// The equivalent of `2 > 5`.
_.gt(2)(5);
// ➜ false

// The equivalent of `_.gt(5, 2)` or `5 > 2`.
_.gt(_, 2)(5);
// ➜ true

まとめ

カリー化というlodash-fpのごく一部の機能をおススメする記事になりましたが、lodash-fpはそれだけでも十分に役に立つライブラリです。

関数型プログラミングを学びたいけど全く知らないライブラリは手が出ない、同じようなアロー関数式があふれている、そういった悩みを持つ方のお役に立てば幸いです。意外とQiitaのlodash記事は少ないようですし...

明日は@rinon2380さんによる「未来の社会人エンジニアに送る、やっておきたい色々なこと。」です。お楽しみに!

参考

lodash/lodash FP-Guide
lodash/lodash Document