applyとreduceの違い

リスト内の数値の合計を計算しようとして

(defun sum (lst)
  (apply #'+ lst))

(sum '(1 2 3 4 5))   ;=> 15

というような関数を書きました。普通に使うぶんには全く問題なかったのですが、長いリスト(具体的には480,000個のdouble-float)を引数にしたら「スタックが足りなくなってしまったよ」というエラーが出て計算できなくなってしまいました。

しょうがないので、以下のようにapplyをreduceに置き換えたところ、問題なく計算ができるようになりました。

(defun sum (lst)
  (reduce #'+ lst))

(sum '(1 2 3 4 5))   ;=> 15

表面上は同じ結果になるのですが、何が違うのでしょうか。トレースをかけて簡単に調べてみました。

CL-USER> (trace +)
(+)

CL-USER> (apply #'+ '(1 2 3 4 5))
  0: (+ 1 2 3 4 5)
  0: + returned 15
15

CL-USER> (reduce #'+ '(1 2 3 4 5))
  0: (+ 1 2)
  0: + returned 3
  0: (+ 3 3)
  0: + returned 6
  0: (+ 6 4)
  0: + returned 10
  0: (+ 10 5)
  0: + returned 15
15

CL-USER> (untrace)
T

なるほど、applyは加算関数をリストの先頭につけた(+ 1 2 3 4 5)を実行するのに対して、reduceは(+ (+ (+ (+ 1 2) 3) 4) 5)のような計算をするのですね。reduceは先頭から一つずつ値を取っていって加算していくので実行時間が必要になりそうですが、長いリストに対しても適用できるのでした。

と、ここで、「あれ、funcallってのもあったよな」と思ったので、HyperSpecへのリンクをメモっておきます。(applyはリストに対して関数適用しますが、funcallは(funcall #'+ 1 2 3 4 5)のように使いますね)