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 applyingf
to the first 2 items incoll
, then applyingf
to that result and the 3rd item, etc. Ifcoll
contains no items,f
must accept no arguments as well, and reduce returns the result of callingf
with no arguments. Ifcoll
has only 1 item, it is returned andf
is not called.If
val
is supplied, returns the result of applyingf
toval
and the first item incoll
, then applyingf
to that result and the 2nd item, etc. Ifcoll
contains no items, returnsval
andf
is not called.
总的来说, reduce 大体上都是对 coll 应用某种函数 f
,其中的状态参数 val
是可选的,起初我以为它的两种用法是这样的:
- 携带状态,对
coll
的每个元素逐一处理。 - 不携带状态,对
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
。