Clojure Reduce 的两种用法


map/reduce 在 Lisp 语言中是一种很常见的用法, Clojure 自然也不例外。

自上次 title-case 的练习之后,我以为自己对 reduce 已经有了较为深入的了解了;恰好最近在 StackOverflow 上刚好遇到一个类似的问题,有个网友想把一个 list 转为一个字符串,就是 Python 中经常见到的 join 方法。

于是我就活学活用,很快用 reduce 写了一个简单的版本:

(second (reduce #(vector nil (str (second %1)
                                  (if (= :begin (first %1))
                                    ""
                                    " -> ")
                                  %2))
                [:begin ""]
                ["a" "b" "c"]))

写完以后,虽然自己也感觉代码有点啰嗦、不是很优雅,但当时也没有细究,就贴出去了。

没想到过了一天,有个网友 leetwinski 指出答案过于复杂,并且贴出了 ta 自己的代码,我这才恍然大悟,意识到自己并没有那么地了解 reduce 的用法。

首先,我们来看一下 reduce docs

(reduce f coll)(reduce f val coll)

f should be a function of 2 arguments.

If val is not supplied, returns the result of applying f to the first 2 items in coll, then applying f to that result and the 3rd item, etc. If coll contains no items, f must accept no arguments as well, and reduce returns the result of calling f with no arguments. If coll has only 1 item, it is returned and f is not called.

If val is supplied, returns the result of applying f to val and the first item in coll, then applying f to that result and the 2nd item, etc. If coll contains no items, returns val and f is not called.

总的来说, reduce 大体上都是对 coll 应用某种函数 f ,其中的状态参数 val 是可选的,起初我以为它的两种用法是这样的:

  1. 携带状态,对 coll 的每个元素逐一处理。

  2. 不携带状态,对 coll 逐个处理。

问题就出在第二点,实际上函数 f 它并不是对 coll 逐个处理,(在 coll 至少包含 2 个元素的情况下)而是在首次被调用时对第1个、第2个元素应用函数,得到结果后,再和第3个元素一起应用函数 f ,它的结果再和第4个元素应用函数……以此类推,每次问题规模都缩小一个。

回到这个 StackOverflow 问题,那么用第二种方法就可以写出更加简洁的代码:

(reduce (fn
          ;; 记得要处理 0 个元素的 coll
          ([] "")

          ;; 处理多余 2 个元素的 coll
          ([a b] (str a " -> " b)))
        ["a" "b" "c"])

当然了,工程实践上请使用 Clojure 内置的 clojure.string/join


comments powered by Disqus