【初心者向け】map, filter, reduce で関数型プログラミングの第一歩【図あり】


イントロ

JavaScript はオブジェクト指向言語ではあるものの、関数型プログラミング言語の影響を少なからず受けている。配列の map, filter, reduce メソッドがその一例である。これらはみな関数を引数に取る高階関数 (higher-order function) と呼ばれるものであり、for ループでよく行われる配列処理のパターンを抽象化したものとみることができる。日常的に行われる配列処理の大半は、これらのメソッドの組み合わせて簡潔に表現できる。

この記事では初心者向けにこれら3つのメソッドの動作を説明する。

Note: 3つのメソッドすべて、元の配列には変更を加えず、別の新しい配列/値を返すことに注意されたい。

map

map メソッド各要素をある関数で変換してできる新しい配列を返す。数学における写像 (map)に対応する。

const new_arr = arr.map(f)

f は関数であり、下の図のように arr の各要素に対して f を適用し、その返り値から新しい配列を生成し、返す。要素の順番は保存される。下の図は配列の各要素を2乗する例である。

対応するコードは以下のようになる。

const arr = [2, 4, 8, 3, 1, 5]

function f(x) {
  return x * x
}
const new_arr = arr.map(f)

/* arrow function バージョン */
const new_arr = arr.map(x => x * x)

for ループで表現すると以下のようになる。

const arr = [2, 4, 8, 3, 1, 5]
const new_arr = []

for (const item of arr) {
  new_arr.push(item * item)
}

filter

filter メソッドある条件を満たす要素だけを取り出して新しい配列を返す。文字通りフィルターで要素を濾すメソッドである。

const new_arr = arr.filter(f)

条件を表す関数 f を引数にとり、ftrue (もしくは truthy) を返すような要素だけからなる新しい配列を返す。ftrue を返せば合格、false を返せば不合格、という具合に各要素の合否を調べ、合格した要素だけを集めて新しい配列を作る。下の図は偶数の要素だけを取り出す例である。

対応するコードは以下のようになる。

const arr = [2, 1, 4, 6, 3, 8]

function f(x) {
  return x % 2 === 0  // 2で割り切れるかどうか
}
const new_arr = arr.filter(f)

/* arrow function バージョン */
const new_arr = arr.filter(x => x % 2 === 0)

for ループで表現すると以下のようになる。

const arr = [2, 4, 8, 3, 1, 5]
const new_arr = []

for (const item of arr) {
  if (item % 2 === 0) {
    new_arr.push(item)
  }
}

reduce

このメソッドは他2つと比べると若干複雑だが、非常に便利な関数である。

reduce メソッド配列を左から見ていって最終的に1つの値を得る。複数の値を含む配列を1つの値へと減らすため、reduce と呼ばれる。accumulate や fold とも呼ばれる。

const result = arr.reduce(f, ini)

例でないとわかりにくいので、和を求める例で説明する。私たち人間が暗算で配列の和を求めようと思えば、普通左から見ていって繰り返し足し算を行うだろう。例えば [4, 1, 2, 3] という配列ならば、初期値は0で、
04を足して4
41を足して5
52を足して7
73を足して10
と計算する。この処理は、途中結果要素を受け取って次の途中結果を得る関数」を繰り返し適用することで表現できる、ということがわかる。この関数が f にあたる

f の第1引数が途中結果、第2引数が現在の要素である。今の場合は f は単に2つの値を足す関数、つまり function (x, y) { return x + y } である。ini は初期値にあたり、和を求める場合は当然 0 だが、積を求めたい場合は 1 となる。図で表すと下のようになる。

これに対応するコードは以下である。

const arr = [4, 1, 2, 3]

function f(x, y) {
  return x + y
}
const sum = arr.reduce(f, 0)

/* arrow function バージョン */
const sum = arr.reduce((x, y) => x + y, 0)

for ループで表現すると以下のようになる。

const arr = [4, 1, 2, 3]
let sum = 0

for (const item of arr) {
  sum = sum + item
}

これだけ見れば、「和とか積みたいな数学的な処理にしか使えないのか」という印象を持つかもしれないが、最初に述べたとおり「配列を左からシーケンシャルに処理する」という形の処理であれば何でもできるということを覚えておきたい。その際、f としてどのような関数を指定すればよいかは上述の例のように考えれば分かるはずである。

例えば下のように配列をオブジェクトに変換したいとき、

const users = [{ "id": "U1321", "name": "John" }, { "id": "U17583", "name": "Mary" }]
// ↓
{ "U1321": "John", "U17583": "Mary" }

reduce を使って次のようにできる。

users.reduce((cur, user) => {
  cur[user.id] = user.name
  return cur
}, {})

注意点

mapfilter は実行するたびに新しい配列を生成する。巨大な配列に対してこのようなメソッドのチェーンを実行すると、無駄にメモリを圧迫しガーベージコレクションを発生させる状態になりかねない。例えば、下の例では2回無駄に配列が作られることになるが、実際には新しく配列を作らずに同じ計算をすることができる。

const arr = [1, 2, ..., 100000000]
const new_arr = arr
  .map(x => x * x)             // <- ここで配列が作られる
  .filter(x => x % 3 == 0)     // <- ここでも
  .reduce((x, y) => x * y, 1)

このようなケースでは for ループ1つでまとめるか、stream ベースで lazy に処理できる RxJSStream.js のようなライブラリを使うのがいいだろう。

もっと例

  • ユーザーの配列から、10歳以下のユーザーの名前の配列を求める
users
  .filter(user => user.age <= 10)
  .map(user => user.name)
  • ユーザーの配列から、20歳以上のユーザーの所持アイテム数の合計を求める
users
  .filter(user => user.age >= 20)
  .reduce((cur, user) => cur + uesr.itemCount, 0)
  • 非同期な(Promise を返す)関数の配列をシーケンシャル(逐次的)に実行する
promises
  .reduce((cur, p) => cur.then(() => p()), Promise.resolve())
// ↓
// Promise.resolve().then(() => promises[0]).then(() => promises[1])...