lodash を使った JavaScript における関数型のデータ処理
「関数型のデータ処理」と呼んでいるのは、
高階関数を使い、遅延評価で行うデータ処理の手法で、
関数型プログラミングでよく使われます。
これは関数型プログラミングでなくても、役に立つ機能であり、
JavaScript でも lodash というライブラリーを使えば、これが行えるようになります。
今回はこの関数型のデータ処理の魅力と lodash でのやり方について紹介したいと思います。
関数型データ処理の魅力
関数型のデータ処理方法は関数型プログラミングでなくても、 役に立つため、関数型言語以外の言語でも積極的に取り入れられるようになって来ました。この辺りのことは最後に説明するとして、 このデータ処理方法の何がよいかというと 短くかつ分かりやすいコードが書けることです。
説明のために、サンプルを挙げてみます。
カンマ区切りの文字列があったとします。まずこれを配列に分割し、 次の処理を行います。
- 要素を数値に変換
- 奇数の要素のみ取得
- 全ての要素の積を算出
従来方式 Case 1
まずは単純に処理を順に行う方法で書いてみます。// 元の配列 [ '3', '2', '9', '6' ] var src = "3,2,9,6".split(","); // 数値に変換 [ 3, 2, 9, 6 ] var map_ary = []; for (cnt in src) { map_ary.push(parseInt(src[cnt], 10)); } // 奇数を取得 [ 3, 9 ] var filter_ary = []; for (cnt in map_ary) { if (map_ary[cnt] % 2 == 1) { filter_ary.push(map_ary[cnt]); } } // 要素の積算 var prod = 1; for (cnt in filter_ary) { prod *= filter_ary[cnt]; } console.log(prod); // 27
従来方式 Case 2
処理を順にやるだけだと流石に無駄が多いので、ループが一回で済むようにコードを直してみます。var src = "3,2,9,6".split(","); var prod = 1; for (cnt in src) { var num = parseInt(src[cnt], 10); // 数値に変換 if (num % 2 == 1) { // 奇数を取得 prod *= num; // 要素の積算 } } console.log(prod); // 27
lodash を用いた方法
処理の内容については後から説明しますが、とりあえず lodash を使って書き直してみます。var src = "3,2,9,6".split(","); var prod = _(src) .map(str => parseInt(str,10)) // 数値に変換 .filter(n => n % 2 == 1) // 奇数を取得 .reduce((t,n) => t * n); // 要素の積算 console.log(prod); // 27lodash を使った書き方では Case 1 のように分かりやすく順に書いていくことができる上、 短く書けます。
それでいて遅延評価の機能によって、実は Case 2 のように効率的に処理しています。
ここで、ついでに何故この処理方法が関数型プログラミングでよく使われるかを説明したいと思います。
関数型では 参照透過性 のため、一度作ったオブジェクトは変更できません。 これは並列処理に強くなるというメリットがあるのですが、 変数の値を変えられないので、ループを回すことができないといった縛りがでてきます。
そのため、高階関数を使ったデータ処理は関数型には必須の機能となります。
lodash
lodash とは
lodash というのは、汎用的に使える機能を集めた JavaScript のライブラリーで、 特に今回紹介する関数型のデータ処理に重きが置かれています。もともと Underscore.js というライブラリーがあったのですが、 意見の相違が原因で分離して開発されることになりました。
Node.js と Io.js のように統合の動きもありますが、こちらの統合はなかなか難しいそうな感はあります。 Underscore.js と lodash は似たような機能を持っていますが、 パフォーマンスを考慮して、 遅延評価を導入しており、個人的には lodash の方がお勧めだと思います。
クライアントサイドでの使用
クライアントサイドで有名な JavaScript のライブラリーを使う場合、 CDN (Contents Delivery Network) といって、公開されているスクリプトファイルを利用するのが、お手軽です。CDN には Google, Microsoft のものや cdnjs, jsDelivr などいくつかあるのですが、 ここでは cdnjs のアドレスを紹介します。
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.min.js"></script>上記のどちらかを html に書いておけば、使えるようになります。 なお、 min とつけている方はコアの機能だけのものです。
使用する場合、もとになった Underscore.js のように lodash では"_"(アンダーバー)を使ってアクセスします。
ちょうど Prototype.js や jQuery が $ を使うような感じです。
使用例 :
Node.js での使用
Node.js で使用する場合は npm コマンドでインストールします。$ npm install lodashインストールしたプロジェクトでは require でロードすれば、使えるようになります。
var _ = require('lodash');require の戻り値を格納する変数の名前は何でもよいのですが、慣例に沿って _ にしています。
ただ、ちゃんとプロジェクトにインストールするにはもう少し手順が必要なので、サンプルコードを例に簡単に手順を紹介します。
まず、適当なディレクトリーを作ってそこにサンプルのファイルをおきます。ファイルは以下のリンクからダウンロード(リンク先を保存)してください。 その後、プロジェクトの作成とインストールを行います。
$ npm init -y $ npm install lodash --saveサンプルを実行は node コマンドで行います。
$ node lodash_sample.js各コマンドの詳しい説明に関しては、以前の記事を見て下さい。
基本的な関数
ここから関数型のデータ処理で重要な 5 つの機能について説明します。- 逐次処理(forEach)
- 写像(map)
- フィルター(filter)
- 畳み込み、縮退(reduce)
- 並び替え(sortBy)
forEach : 逐次処理
データ処理の中でもっとも基本的な処理は逐次処理です。これには forEach 関数を使います。 引数に関数を渡すことによって、その関数をコレクションの各メンバーに適用していきます。
以下の例では配列の要素を 1 つづつ出力します。
_.forEach([3,2,9,6], function(elem) { console.log(elem);});関数の最初の引数に対象となるデータを渡しています。
forEach は一番最初にあげたサンプルのように書くこともできます。 関数を連結する場合に使うのですが、関数の連結は少し注意点があるので、主要な関数の紹介の後に説明したいと思います。
_([3,2,9,6]).forEach(function(elem) { console.log(elem);});逐次処理は for ... in と同じようなものです。
ただ、 JavaScript の for ... in は他の言語と違い、ループの変数が数値のインデックスであり、 かなり不自然です。 それに対し、 forEach と高階関数を使えば、各要素を変数として受け取ることになり、 分かりやすいコードになるのではないかと思います。
map : 写像
写像というのは map, mapping とも呼ばれる処理で、 配列などのコレクションのメンバーに一つずつ関数を適用して 戻り値で新しいコレクションを作ります。lodash では、そのまま map という関数名です。
_.map([3,2,9,6], function(elem) {return elem*2;}); // [ 6, 4, 18, 12 ]map に渡す関数は、各要素を引数にとる関数で、その戻り値が新しいコレクションの要素となります。 最初のサンプルのように渡す関数の戻り値の型を変えれば、 型を変えた新しいコレクションを作ることもでき、用途の広いメソッドです。
forEach, map などには渡す関数は、通常の名前付きの関数でもいいですし、 サンプルのような無名関数 でもかまいせん。
また、無名関数は ES6 から アロー関数 というより短い形式で書くこともできるようになっています。
_.map([3,2,9,6],
elem => elem*2 );
これは function という単語や return を書く必要がなくなり便利です。
以降の例ではアロー関数を使った書き方にしたいと思います。CoffeeScript や C# などを使ったことのある人には馴染みのある書き方だとは思いますが、 より詳しく知りたい方は以下のサイトを見てください。 また、引数の数が違う関数やオブジェクトのメソッドをデータ処理に使いたい場合は次の記事を参考にしてください。
filter : フィルター
フィルターはコレクションの中から条件にあう要素を取り出す処理です。データ処理で重要なメソッドをさらに絞るとすると、前節の map とこのフィルターが、 特に重要度が高いです。
これも、そのまま filter という名前になっています。
_.filter([3,2,9,6], elem => (elem % 2) == 1);
データ処理に渡すのは基本的に関数です。 ただ、 JavaScript ではコレクションの要素がオブジェクト(連想配列)となることが多いせいか、 lodash では filter のように "条件を返す関数" を引数にとる関数では、 特殊な書き方ができます。
関数ではないものを渡すと matches, matchesProperty が使われるようになっています。
var objs = [{'name': 'Tanaka', 'age': 35, 'enrolled': true}, {'name': 'Yamada', 'age': 30, 'enrolled': false}, {'name': 'Satou', 'age': 41, 'enrolled': true}]; // matches 呼び出し _.filter(objs, {'age': 35, 'enrolled': true}); // [ { name: 'Tanaka', age: 35, enrolled: true } ] // matchesProperty 呼び出し _.filter(objs, ['age', 35]); // [ { name: 'Tanaka', age: 35, enrolled: true } ]さらに bool 値のプロパティーを指定すると、要素のプロパティーの値が条件として使用されます。
_.filter(objs, 'enrolled'); // [ { name: 'Tanaka', age: 35, enrolled: true }, // { name: 'Satou', age: 41, enrolled: true } ]
reduce : 畳み込み(縮退)
畳み込みもしくは縮退は 要素を順に取得し、それらを計算に使った結果を返す処理です。畳み込みは他言語では fold, reduce, inject などの関数名が使われます。 lodash では縮退の方の reduce という関数名です。
var src = [3,2,9,6]; _.reduce(src, (sum, elem) => sum + elem); // 20 _.reduce(src, (max, elem) => (max < elem) ? elem : max); // 9reduce に渡す関数は 2 つの引数を取り、 2 つ目が各要素で、 関数の戻り値が次に呼ばれた時の 1 つ目の引数です。 これにより、各関数の結果を重ねていったものが reduce の戻り値として得られます。
合計の例を順に記述すると次のようになります。
{3, 2, 9, 6} (3, 2) => 3 + 2 ↓ (5, 9) => 5 + 9 ↓ (14, 6) => 14 + 6 ↓ 20渡した関数は 2 つの目の要素から呼ばれ、最初の引数 1 つ目の要素になります。 これは初期値も引数として指定して、最初の要素から処理することもできます。
_.reduce([3,2,9,6], (count, elem) => count+1, 0); // 4map, filter がコレクションから新しいコレクションを返す関数の基本なのに対し、 reduce はコレクションの各要素から値(スカラー値)を算出する関数の中でもっとも基本的な関数です。
sortBy : 並び替え
sortBy は要素を並び替えた新しいデータを返す関数です。ソートでは比較関数を渡すのが他の言語では一般的なのですが、 sortBy は By とついているように比較するためのプロパティーを指定します。
var objs = [{'name': 'Tanaka', 'age': 35}, {'name': 'Yamada', 'age': 30}, {'name': 'Satou', 'age': 41}]; // age の値でソート _.sortBy(objs, 'age'); // { name: 'Yamada', age: 30 }, // { name: 'Tanaka', age: 35 }, // { name: 'Satou', age: 41 } ]比較に用いる値はプロパティー名での指定だけでなく、関数を渡して決めることもできます。 これを使えば、逆順ソートなどもできます。
// age で逆順ソート _.sortBy(objs, elem => elem.age * -1); // [ { name: 'Satou', age: 41 }, // { name: 'Tanaka', age: 35 }, // { name: 'Yamada', age: 30 } ]次の例は最初に紹介すると分かりづらいかなと思って、ここに持ってきました。 sortBy は比較に使う関数を渡すのではなく、比較に使う値を決める関数を渡す ということをおさえていると渡す関数の定義の仕方が理解しやすいと思います。
var strs = ["foo", "bar", "BAZ", "qux"]; _.sortBy(strs); // [ 'BAZ', 'bar', 'foo', 'qux' ] _.sortBy(strs, elem => _.toLower(elem) ); // [ 'bar', 'BAZ', 'foo', 'qux' ] // _.sortBy(strs, _.toLower); でも可また、一つのプロパティーの値だけでは同じ値になってしまうような場合、 複数指定することもできます。
var points = [{'x': 2, 'y': 8}, {'x': 5, 'y': 1}, {'x': 2, 'y': 4}]; // x でソートし、x が同じなら y でソート _.sortBy(points, ['x', 'y']); // [ { x: 2, y: 4 }, { x: 2, y: 8 }, { x: 5, y: 1 } ]
その他の関数
データ処理の定番の関数以外で、抑えておいた方がいいかなと思うものも挙げて置きます。- size
- 要素数の取得
- find, findLast
-
指定した条件に最初(最後)にマッチする要素の検索。
検索もデータ処理ではよく行う処理ですが、 条件にあうすべての要素を取得するのがフィルター(filter)で、 最初の要素を取得するのが find です。_.find([3,2,9,6], elem => (elem % 2) == 1); // 3 _.findLast([3,2,9,6], elem => (elem % 2) == 1); // 9
- includes
-
要素を含んでいるかの判定。
_.includes([3,2,9,6], 2); // true
- some, every
-
要素どれか一つ(すべて)が条件を満たすかどうかの判定。
_.some([3,2,9,6], elem => (elem % 2) == 1); // true _.every([3,2,9,6], elem => (elem % 2) == 1); // false
result = _.size([3,2,9,6]); // 4
Array はそのまま Array のオブジェクトですが、対象が Collection の場合、 Array だけでなく配列のように振る舞うデータ構造すべてに使えます。
なお、今回の記事では対象が Collection の関数だけ紹介しています。
メソッドの連結と遅延評価
メソッドの連結
関数の連結を行う場合、最初に配列(Array)を lodash のオブジェクト(_) に引数として渡すか、 chain 関数に渡してから連結します。_.chain([3,2,9,6]) // _([3,2,9,6]) と同じ .filter(elem => (elem % 2) == 1) .map(elem => elem * 2) .value(); // [ 6, 18 ]最初の _() や chain() の関数が返すオブジェクトはシーケンス(Seq)です。
シーケンスの filter メソッドが返すオブジェクトもシーケンスなので、 さらに map で連結できるようになっています。 関数の連結はシーケンスのメソッドの連結(method chain)によって実現されています。
ただ、このシーケンスは配列ではありません。 最後に forEach などで逐次処理したり、 reduce で値の算出に使う場合はいいのですが、 新しく作った配列を欲しい場合は value で配列に戻す必要があります。
遅延評価
関数を連結して書くとメモリーがもったいないと思われる人もいるかもしれません。 しかし、一番最初の例にあげたように内部的には Case 2 のように効率的に処理しています。これを実現しているのが 遅延評価(Lazy evaluation) の機能です。
連結時のシーケンスは 必要になるまで実行(評価)されないようになっています。 前節の例では value で配列に戻すところが実行のタイミングです。
連結の処理は一見、次のように処理してるように見えます。
filter map {3, 2, 9, 6} → {3, 9} → {6, 18}しかし、実際には遅延評価により、メソッドごとに結果をためるのではなく、 1 要素ずつ流すように次のメソッドに渡していきます。
filter map 3 → 3 → 6 2 → ☓ 9 → 9 → 18 6 → ☓遅延評価の効果をもっとわかりやすくみたいという方は次のサイトがお勧めです。
英語の記事ですが、画像(GIF 動画)をみているだけも理解が深まると思います。 ちなみに画像に出てくる take は先頭から指定した数だけ要素を取り出すメソッドです。
なお、基本処理にあげた sortBy は遅延評価において少し特殊です。
ソートという処理を考えたらわかると思いますが、 ソートは全要素が揃わないと完了しない処理です。 そのため、 sortBy に関しては流れるように次に渡すのではなく、 そこで一旦要素が溜められることになります。
プログラミング言語の傾向
最後に lodash で遅延評価、高階関数を使った関数型のデータ処理について、 最近のプログラミング言語での流れを紹介します。関数型言語
高階関数を使ったデータ処理は LISP で古くから行われてきた手法です。 ただ、遅延評価や参照透過性といった考え方がなく、 最近では LISP は関数型言語には入れないことが多いです。しかし、 Haskell, Scala, F# などの最近の関数型言語では、 遅延評価、高階関数を使ったデータ処理は基本的な機能としてあります。
LISP も今の意味で関数型といえる Closure が出てきています。 特に純粋関数型言語である Haskell は特定のデータ処理だけでなく、 全体が遅延評価されるようになっています。
関数型以外の言語
高階関数を使ったデータ処理はコードを短くかつわかりやすく記述することができます。 そのため、 Ruby, Python をはじめ多くの言語で取り入れられてきました。 特に C# の LINQ では遅延評価もできます。 Java も Java 8 では Stream ができ、 C++ でさえ C++14 以上であれば関数型のデータ処理ができる Streams のライブラリーが使えますこれからのプログラミングでは一般的な機能になって来ているようです。
- C# やるなら LINQ を使おう | プログラマーズ雑記帳
- Java Streams, Part 1: java.util.stream ライブラリー入門
- C++14 Streams を使った関数型のデータ処理 | プログラマーズ雑記帳
特にコードブロックは高階関数を使ったデータ処理としてはもっともエレガントに書ける記述スタイルではないかと思っています。 ただ Ruby の遅延評価は Ver. 2.0 から導入された機能で、後付な感じがちょっと残念です。
しかし、 Ruby の作者である Matz(まつもとゆきひろ) さんが、 このデータ処理を全面に押し出した関数型言語の stream を公開されています。(まだプロトタイプですが)
- Rubyist Magazine - 無限リストを map 可能にする Enumerable#lazy
- GitHub - matz/streem: prototype of stream based programming language
JavaScript と altJS
話を JavaScript に戻すと、 ES6 では Array オブジェクトに map, filter といったメソッドが追加され、 lodash 抜きでも高階関数を使ったデータ処理ができるようになっています。 ただ、遅延評価については特に書いてないようなので、 実装にもよりますが、おそらくできないでしょう。となってくると、 CoffeeScript のような altJS で自然に書いて、 JavaScript に変換するときに lodash を使ってくれないかなと思います。
実際、そういった言語はあります。それが RedScript で、 以前に RedScript の紹介記事を書いた時はまだまだな言語でしたが、最近では完成度が上がってきた感じです。
(なお、 redscript.org のアカウントがカードローン系のサイトに取られてしまったみたいなので、 検索するときには注意してください)
- RedScript : Ruby 風の JavaScript 変換言語 | プログラマーズ雑記帳
- GitHub - AdamBrodzinski/RedScript: A Ruby Flavored Language
- 関連記事
Facebook コメント
コメント