Ruby
JavaScript
Scala
FunctionalProgramming
Rustlang

fold / reduce のススメ(あるいは高階関数のススメ)

最近のプログラミング言語には、多くの場合、コレクションをたどるループ構文に加えて、コレクションを「折りたたむ」ための関数が用意されています。

たとえば、JavaScriptでは、

array.reduce((sum, x) => sum + x, 0) // 0はあえて明示した

で、 array の合計要素が計算できます。また、Scalaでは

list.foldLeft(0){(sum, x) => sum + x}

と書けますし、Rustでは

array.fold(0, |sum, x| sum + x)

と書けます。Rubyでは

 array.inject(0){|sum, x| sum + x}

と書けます。このような例はあまりにもたくさんあるのでこれくらいにしておきますが、色々な言語で使える関数であることは間違いありません。しかし、これらの関数を使うことがなぜ良いことであるかの説明をそんなに見たことがなかったので、自分で書いてみることにしました。

JavaScriptで説明することにします。

sum の例を JavaScript で書くと次のようになるでしょう:

var sum = 0
array.forEach((x) => {
    sum += x
})

あるいは、古典的な for 文を用いて、

var sum = 0
for(var i = 0; i < array.length; i++) {
  sum += array[i]
}

と書くことになるでしょうか。もちろん、これでも構わないのですが、より楽に書きたい、書ければいいというのが本音です。そこで登場するのが reduce です。

reduce という関数が何をするかを知っていれば、という条件付きではあるのですが、

array.reduce((sum, x) => sum + x, 0)

は直接「やりたいこと」を明示できています(配列の左側から順に足し合わせた結果が全体の合計値である、という意味で)。さらに繰り返しの外側に対して変数を定義することなく、全体の処理を定義することができています。

言い換えると、 forEachfor でループを使って計算するより抽象度が高いです。特に、 reduce / foldLeft は、左からの演算結果を順に蓄積していくような構造になっています1。ASCII Artにすると、

       9
     /  \
    4    5
   / \
  1   3  
 / \
0   1

のようになります(ここでは、 [1, 3, 5]sum を取る場合について描写したつもり)。

このような、左からの累積によって全体の結果を決定するような処理は、結構頻繁に出てくるので、それを毎回毎回ループを書いて処理するよりも、 reduce / foldLeft で処理した方が頭を使わなくて済むし(その関数を知っていれば)、より読みやすいので、皆さん、もっと reduce / foldLeft を使いましょう。

ちょっと見方を変えてみると、ループ構文というのはある種の goto のようなもので、万能ではあるのですが、特定のパターンを表現するのは不向きです。 reduce はコレクションを特定の順序で計算する場合にしか使えませんが、そのパターンに当てはまる場合において、より読みやすいコードを提供することができます。

このような言説に対して、 reduce は読みづらい、という方も居るのですが、私としては、それは、単にライブラリ関数として reduce が頭に登録されていないということであって、使い慣れていれば自然に消滅する類の違和感だと思います)。

もちろん、より抽象度が高い方が意図を直接的に表現できていい、ということは、 sum が関数として用意されていれば、

array.sum()

とした方が読みやすいということでもあるわけで、そのようなライブラリ関数があればより積極的に使うのが良いでしょう。ただし、このような関数を一般に用意するには、いわゆる型クラス的な機能が必要になることが多く、データ型に依存しない形でこのような関数を用意するのはやや難しいことがあるかもしれません。

また、 reduce 以外にも、コレクションに対する操作を抽象化した処理は色々あり(たとえば map など)、それらの関数も積極的に使えた方が良いでしょう。


  1. foldRightのことは置いておきます