このページの本文へ

JavaScriptプログラマーならMap()とReduce()で関数型プログラミングを始めてみない?

2016年05月10日 12時49分更新

文●David Green

  • この記事をはてなブックマークに追加
本文印刷
取っつきにくい印象のある関数型プログラミング。でも、論より証拠。ECMAScript 5でサポートされたMap()とReduce()を使うと、こんなにすっきりとしたコードが書けるのです。

本記事はPanayiotis VelisarakosTim SeverienDan Princeが査読を担当しています。最高の内容に仕上げるために尽力してくれたSitePointの査読担当者の皆さんに感謝します。

ECMAScript 6のすばらしい新機能の使い方が話題を集めると、(1つ前のバージョンである)ECMAScript 5から追加された、関数型プログラミングをサポートする優れた機能について忘れがちになります。たとえば、JavaScriptに組み込まれている配列(Array)オブジェクトのネイティブメソッド、map()reduce()です。

まだmap()reduce()を使ったことがなければ、これをきっかけに始めてみましょう。最新のJavaScriptプラットフォームはほぼECMAScript 5を標準でサポートしています。MapとRedueメソッドを使うと、コードがぐっとすっきりし、読むのも管理も簡単になり、関数をもっと使いこなせるようになります。

パフォーマンスにおける注意点

当然のことですが、コードの読みやすさや管理のしやすさを追求するだけでなく、コードを呼び出すときのパフォーマンスについても考慮しなくてはなりません。現在、ブラウザーはループ処理のようなちょっと厄介な従来の技術を使って、より効率よく表示しようとしています。

私はいつも、読みやすく、管理しやすいコードを書くことを念頭に置いています。実際使ってみて問題が発生したときにパフォーマンスを最適化します。早すぎる段階での最適化は厄介なことになると考えています。

また、将来的にブラウザーがより進化した場合、JavaScriptエンジンも改善されるので、map() reduce() のようなメソッドの利用は、十分に検討する価値があると考えています。パフォーマンスの問題の壁にぶつからないかぎり、私は気楽にコードを書くことを優先し、本当に必要なときにだけパフォーマンスのマイナーチェンジをするようにしています。

Mapメソッドを使う

Mapメソッドは、配列のすべての要素を操作し、変換された内容と同じ長さの新しい配列を生成する、基本的な関数型プログラミング技術です。

もう少し具体的に、簡単な例で説明しましょう。たとえば、単語と文字数の配列です。単語の配列を文字数による配列に変換します(これは高性能のアプリケーションで必要になる複雑で難しい例ではありませんが、単純なケースでの動きを理解しておくと、実際に利用するときに対処しやすくなるでしょう)。

配列でのforループの記述は、こんな感じになります。

var animals = ["cat","dog","fish"];
var lengths = [];
var item;
var count;
var loops = animals.length;
for (count = 0; count < loops; count++){
  item = animals[count];
  lengths.push(item.length);
}
console.log(lengths); //[3, 3, 4]

ここでは変数をいくつか定義しただけです。つまり、単語が書いてあるanimals配列、処理を実行した結果が入る空のlengths配列、配列の各ループ内でこれから処理する各項目を一時的に格納するitem変数を定義しました。forループを最適化するために、一時的な内部count変数とloops変数で、forループを設定しました。それから、animals配列内の単語文字数を数えるまで、項目ごとに繰り返しを実行しています。各単語ごとに文字数の長さを計算し、それをlengths配列にプッシュしました。

備考:おそらくitem変数がなくても、animals[count]の文字数を直接、なにも代入せずにLengths配列にプッシュすると、もう少し簡潔に処理できたはずです。そうすればコードはもう少し短くなりますが、このような単純な例でもコードはもっと読みにくくなっていたでしょう。同様に、もっと高性能で、少し複雑にするには、分かっているanimals(訳注:原文ではanimaLs配列内の文字数を、new Array(animals.length)(訳注:原文ではnew Array(animaLs.Length)としてlengths(訳注:原文ではLengths配列を初期設定して、プッシュする代わりにインデックスで項目を挿入することもできました。実践でどうやってコードを使いたいかによります。

この方法は技術的に間違っていませんので、どのJavaScriptエンジンでもうまくいくでしょう。ただし、一度map()の使い方を知ると、使いにくいと感じるかもしれません。

map()を使った方法を見てみましょう。

var animals = ["cat","dog","fish"];
var lengths = animals.map(function(animal) {
  return animal.length;
});
console.log(lengths); //[3, 3, 4]

この方法でも、動物の種類を入れるanimals配列の変数を定義することから始まっています。しかし、そのほかに指定したした変数はlengthsだけで、その値は直接、animals配列の各要素に無名のインライン関数をマッピングした結果に割り当てています。この無名の関数は各動物名に実行され、文字数が返されます。その結果、lengthsは、各単語の文字数通り元のanimals配列と同じ配列になりました。

この方法にはいくつか特徴があります。まず、元のやり方と比べてかなり短いコードで済むこと。次に、変数の指定がほとんどいらないこと。変数が少ないほど、グローバル名前空間の複雑化を防ぎ、同じコードを使っている他のパートで、同じ名前が付いた変数が衝突するのを防げます。さらに、最初から最後まで、変数の値を変えなくてよいのです。関数型プログラミングの知識が深まるにつれて、定数とあとから変更できない変数(immutable variables)を使ったこの威力に気づくでしょう。始めるのに早すぎることはありません。

もう1つのメリットは、名前がついた関数を分割することと、プロセスの中でより簡潔なコードを生成することで、汎用性が高まることです。無名のインライン関数は厄介にみえてコードの再利用が面倒なので、名前付きのgetLength()関数を定義して、次のようなコンテキストでも使えます。

var animals = ["cat","dog","fish"];
function getLength(word) {
  return word.length;
}
console.log(animals.map(getLength)); //[3, 3, 4]

すっきりしているでしょう。マッピングをツールキットの一部として作ることで、コードが1つの新しい関数にまとまりました。

関手(Functor)とはなにか?

興味深いことに、配列オブジェクトにマッピングが追加されたことで、ECMAScript5では基本的な配列タイプが充実した関手(Functor)に変わり、関数型プログラミングがより身近なものになりました。

関数型プログラミングの元々の定義によると、関手は以下の3つの基準を満たしています。

  1. 1組の値がある
  2. 各要素に実行するMap関数を実装している
  3. Map関数は同じサイズの関手を返す

これはJavaScriptの次のステップで学ぶことになるでしょう。

関手についてもっと知りたい人は、Mattias Petter Johanssonが作成した素晴らしい映像を見てください。

Reduceメソッドを使う

reduce()メソッドもECMAScript 5の新しい機能で、map()に似た機能ですが、reduce()は別の関手を生成する代わりに、どのタイプの結果にもなりうる1つの結果を生成します。たとえば、animals配列のすべての単語の文字数の合計を計算するとしましょう。おそらくこんな感じで始めますね。

var animals = ["cat","dog","fish"];
var total = 0;
var item;
for (var count = 0, loops = animals.length; count < loops; count++){
  item = animals[count];
  total += item.length;
}
console.log(total); //10

最初に配列を定義したあと、現在の文字数合計を示すtotal変数を作成し、最初は0にします。また、forループを実行してanimals配列にある各要素に繰り返し処理をするためのitem変数、ループカウンターのためのcount変数、繰り返し処理を最適化するloops変数も作成します。それからforループを実行してanimals配列にある要素すべてに繰り返し処理をして、各文字数をitem変数に割当てます。最後に、各要素の文字数を合計にします。

ここでもこの方法に技術的な間違いはありません。配列で始まり、結果で終わっています。しかし、reduce()メソッドを使うと、もっとずっと簡単になります。

var animals = ["cat","dog","fish"];
var total = animals.reduce(function(sum, word) {
  return sum + word.length;
}, 0);
console.log(total);

何をしているかというと、新しいtotal変数を定義し、2つのパラメータを使ってanimals配列に還元しています。この2つのパラメータとは無名のインライン関数と現在の文字数合計の初期値0です。還元することで配列の中の各要素に対して関数を実行し、その値を文字数合計に加算し、繰り返して値を渡していきます。ここでインライン関数は2つのパラメータを取得します。これまでの集計と現在配列で処理している単語です。関数はtotalの現在値を処理している単語の文字数に足します。

備考:reduce()の2つ目の引数を0に設定することで、total変数に数字が入ることに注目してください。この2つ目の引数がなくてもreduceメソッドは動きますが、結果が期待どおりになるとは限りません(現在の文字数合計を外してもJavaScriptが意図したとおりに動くかは試して確認してください)。

reduce()メソッドでインライン関数の定義を統合するので、必要以上に複雑に見えるかもしれません。無名のインライン関数を使う代わりに、名前付きの関数を定義してもう一度やってみましょう。

var animals = ["cat","dog","fish"];
var addLength = function(sum, word) {
  return sum + word.length;
};
var total = animals.reduce(addLength, 0);
console.log(total);

このコードの方が少し長いですが、必ずしも長い方が悪いというわけではありません。reduceメソッドを使った処理が少し分かりやすくなっているはずです。

reduce()メソッドは2つのパラメータを取得します。配列の各要素に適用するaddLengthという新しい関数と現在の文字数に合計するための初期値0です。addLength関数は、現在の文字数と、処理した文字数をつなげる2つのパラメータを取得します。

Conclusion  まとめ

map()reduce()を日常的に使うようになると、コードが分かりやすくなり、汎用性も高まり、管理しやすくなるでしょう。そしてJavaScript関数の技術をより使いこなせるようになります。

map()reduce()メソッドはECMAScript 5に追加されたたった2つの新しいメソッドです。しかし、この2つの新機能がもたらしたコードのクオリティと開発者の満足度の向上は、パフォーマンスの一時的な効果をはるかに上回っているでしょう。関数の技術を磨いて、アプリケーションにmap()reduce()が適しているか判断する前に、実践の中でその効果を確かめてください。

(原文:Using Map and Reduce in Functional JavaScript

[翻訳:和田麻紀子]
[編集:Livit

Web Professionalトップへ

WebProfessional 新着記事