🎃

Reduce関数を使ってみよう

2022/10/31に公開約2,800字3件のコメント

Reduce関数、使えるとなんだかいい気分。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce

基本の形

array.reduce((previousValue, currentValue) => {} , initialValue)

配列から配列以外を含む任意の値を作成したい場合に利用します。配列が空で、initialValueもない場合はエラーになります。

デモ

まずは例を見た方が理解しやすいと思うので、一つ紹介します。以下は配列の要素を合計する処理です。

// 対象の配列
const array = [1, 2, 3];
// 初期値
const initialValue = 0;

//処理
const output = array.reduce((prev, current) => {
  return prev + current
}, initialValue);

console.log(output) // -> 6

中で行われている処理は以下の通りです。

// 1回目
{ "prev": 0, "current": 1 }
// 2回目
{ "prev": 1, "current": 2 }
// 3回目
{ "prev": 3, "current": 3 }

//結果
6

引数

array.reduce(callbackFn, initialValue)

コールバック関数 では全部で4つの引数を受け取ります

previousValue
処理前の値(直前の処理結果)。初回の呼び出しでは、initialValue が指定された場合はその値が、そうでない場合は配列の0番目の要素の値が入ります。
currentValue
現在の配列要素の値。初回の呼び出しでは、initialValue が指定された場合は 配列の0番目の要素が、そうでない場合は 配列の1番目の要素の値が入ります。
currentIndex
currentValue の位置。
array
 対象の配列。

InitialValueは、コールバック関数が初めて呼び出されたときの previousValue の初期値です。

良い点

同様の処理は forforEach でも出来ます。デモの例を forEach に置き換えるとこんな感じになります。

const array = [1, 2, 3];

let sum = 0

array.forEach((element) => {
  sum += element
})

同様の結果は得られますが、これだと一時的にループ用の変数 i を用意する必要があります。悪くはないかも知れませんが、Reduce の方がより簡潔でわかりやすいと思いますし、const が使えるのも良い点ですよね。

forEach について調べていると以下記事を見つけました。
https://qiita.com/diescake/items/70d9b0cbd4e3d5cc6fce

用途

  1. 最大値を求める(最小値も対応可)
const arr = [10, 3, 4, 9]
 
const output = arr.reduce((prev, current) => {
  return prev < current ? current : prev 
}, 0)

// -> 10
  1. オブジェクトのある要素を合計
const arr = [
  { name: "apple", plice: 100 },
  { name: "banana", plice: 150 },
  { name: "grape", plice: 300 },
]

const output = arr.reduce((prev, current) => {
  return prev + current.plice
}, 0)

// -> output = 550
  1. オブジェクトをグルーピング(応用)
const posts = [
  { title: "post 1", tag: "red" },
  { title: "post 2", tag: "blue" },
  { title: "post 3", tag: "blue" },
  { title: "post 4", tag: "red" },
]

const output = posts.reduce((prev, current) => ({
  ...prev,
  [current.tag]: [...(prev[current.tag] || []), current]
}), {});
{
  "red":[
    { "title": "post 1", "tag": "red" },
    { "title": "post 4","tag": "red" },
  ],
  "blue":[
    { "title": "post 2", "tag": "blue" },
    { "title": "post 3", "tag": "blue" },
  ]
}

番外編

普段はTypeScriptで開発しているのですが、用途で3番目に紹介した例だとTypeErrorになったので、自分なりに型を調べてつけてみました。(もっと良い型やご指摘等あればコメントください🙇)

type Post = {
  title: string
  tag: string
}

const posts = [
  // ...
] as const

const output = posts.reduce((prev, current) => ({
  ...prev,
  [current.tag]: [...(prev[current.tag] || []), current]
}), {} as { [key in "red" | "blue"]: Post[] }

Discussion

以前、記事書いたことがあります。

[JavaScript] reduceは可読性が悪くループで置き換え可能なので使わないようにしている話 - Qiita
https://qiita.com/standard-software/items/3254c19ed5229f3aba9a

例ですが自分ならこのように書くな、というコード書いておきます。

1,min,max

const arr = [10, 3, 4, 9]
 
let _max = -Infinity;
for (const item of arr) {
  if (_max < item) { _max = item }
}
const max = _max;

const output = max;

あるいはもっと簡単に

const min = Math.min(...arr)
console.log({min})

const max = Math.max(...arr)
console.log({max})
const arr = [
  { name: "apple", plice: 100 },
  { name: "banana", plice: 150 },
  { name: "grape", plice: 300 },
]

const sum = array => {
  let result = 0;
  for (const item of array) {
    result += item;
  }
  return result;
}

const output = sum(arr.map(i => i.plice));
3.

type Post = {
  title: string
  tag: string
}

const posts: Post[] = [
  { title: "post 1", tag: "red" },
  { title: "post 2", tag: "blue" },
  { title: "post 3", tag: "blue" },
  { title: "post 4", tag: "red" },
]

const uniq = (array: (string | number | boolean)[]) => {
  const result: typeof array = [];
  for (const item of array) {
    if (!result.includes(item)) {
      result.push(item);
    }
  }
  return result;
}

const result: { [prop: string]: Post[] } = {};
for (const item of uniq( posts.map(item => item.tag) )) {
  result[item as string] = posts.filter(post=>post.tag === item)
}

console.log({result})

sum や uniq は汎用的なので、事前に作っているべきだったり、あるいはその場ですぐに作れます。

で、段階にそった考え方で、mapしたものをuniqとって、
それでループしてfilterで持ってくる、と考える方が自然で自然ゆえに読みやすいと感じます。

読みやすくなければ不具合を入れ込んだとしてもわかりにくいので、
途中でconsole.logをいれて値をトレースも難しいです。reduce

また、const は、1.の例でもあるように、letしてからconstすればよかったり
あるいは古のテクニックの無名関数即時実行とかすればいいのでそれもメリットにはなりません。

const arr = [10, 3, 4, 9]

const max = (() => {
  let max = -Infinity;
  for (const item of arr) {
    if (max < item) { max = item }
  }
  return max;
})();

console.log({max})

あと、forEachは、breakできないので、今どきは for of 使うのがいいと思います。

なるほど。。
記事拝見しまして、一概に可読性といってもチームのレベルや規模によって変わってくると思うので難しいところですが、概ねその通りだと思いました。
何でもかんでもReduce関数を使うのは避けるべきだと私も思います。同時に、本当に便利なシチュエーションは限られていると聞いたことがあって、そういう場面、例えば例3とかなら使っても良いのかなと個人的には思いますが、その点どうお考えでしょうか?使っても良い場面についてのご意見はありますか?

調べる中で、初学者ながら、こんな使い方もあるんだ!という発見のもとつい紹介しちゃった部分もあって、このようにご指摘いただきとても助かりました。次回以降は気をつけます。学びのあるコメント有難うございます。

どうなんでしょうか、自分は複雑な部分は速攻関数化してしまうので、reduceは使わないですリンク先でいろいろ書いた通りです。
for of 使っているほうが、仕様変更あったりして、continueやbreakをいれなければいけなくなった場合も修正に便利です。
breakが必要になった、という場合、reduceから for of にしてから break をするとかって修正の労力が辛いので、はじめからfor ofにしとく方が少し得かなとも思います。

人が書いたコードを正確に理解するためにreduceも読めるようにしておいている感じです。

でもまあ、単に私はそう思うだけですので、動くんだから、自由にされるのがよいでしょう。
私のは単なるひとつの意見として、ご自身の意見を作っていってこだわりを持たれるとよいと思います。

初学者とのことですが、ひとつ、また単なる私の意見をいっておきますと
プログラミングって学んでいる方は、相当数が初学者や入門者だったりします

で、そういう段階の相当多くの方が
プログラミングがパズルのように思い、パズルを解けることがよいことだと思うようです。

Reduce関数、使えるとなんだかいい気分。

と書かれていますが、たぶんそんな感じなんではないかなと思いました。

例3についても、reduce使って短くかけていますが、コードは短くかけたところで可読性がわるければまずいと思うんです。むしろ短く書くことにほとんどメリットはありません。

すっきりと短く書くなら事前に関数を用意しておくのも手で、lodashならgroupbyという関数があったり、自分は自作しています。

https://github.com/standard-software/partsjs/blob/master/source/array/_group.js
https://jsbin.com/picerefake/edit?html,js,console
出力結果は違いますが、ほぼ同じ処理です。

もう20年もやっていると、シンプルイズベストなコードの方が読みやすくて重要だなと感じ
reduceよりは、for of だろうなと、思うという一つの意見です。

ログインするとコメントできます