- JavaScript玩转Clojure大法之 - 并发编程
- JavaScript玩转Clojure大法之 - Transducer
- JavaScript玩转Clojure大法之 - Trampoline
- JavaScript玩转Clojure大法之 - Macro (1)
通过上一篇Clojure风格的JavaScript并发编程介绍了如何用JavaScript享受到Clojure在并发编程的优势. 我决定 写一系列关于如何用JavaScript玩转Clojure大法的文章. 这回要用JavaScript玩转另一个 Clojure全新的概念 -- Transducer.
Transducer 是 Rich Hickey[1] 高调宣布 的在Clojure 1.7 版本加入的又一大法. 在之前的另一个概念 Reducer 却没那么 高调. 在解释transducer之前, 先看看什么是Reducer, 如果能看懂, 再接着看Transducer.
Reducer
说道reduce这个词, 想必JS Developer大多会用过underscore2的reduce方法, 大概形式是这样
_.reduce(fn, 0, [1,2,3])
大概意思是初始为0, 应用fn到每一个collection(检测coll)元素上,得到一个新的值.
如果加上map, 比如(我要开始用mori[3] 了)
reduce(sum, 0, map(inc [1,2,3]))
Terminology:
- reducing 函数: 用来reduce的函数, 比如sum
- transform: 变换, 从一个函数变另一个函数
- xf: xform, transform 函数
- reducible: 可被reduce的,也就是实现reduce接口的,比如所有的collection
让我们一步一步分析一下这次reduce到底干了什么
- map 函数 inc 到 coll 每一个元素, 得到一个新的 coll
[2,3,4]
- reduce 把新coll的每个元素用sum函数, 得到一个新的值.
好吧这就是reduce了, 用一个reducing函数sum去计算coll得出一个新的值.
来看看更好的解法
transform
reduce函数需要等待map返回新的coll后才能reduce, 那么可不可以一步直接算出来呢?
假如我们有一个函数xf可以变换reducing函数(上例的sum是reducing函数)的形式, 比如
xf(reduceFn) -> anotherReduceFn
再假如我们的新map函数可以做这种转换
map(inc)(sum) -> aShinyNewReduceFn
map 函数的简单transform实现可以这样实现,如果你感兴趣的话
function map(fn){ return function(reduceFn){ return function(result, input){ reduceFn(result, fn(input)) } } }
那么我们之前的reduce就可以写成
reduce(map(inc)(sum),0,[1,2,3])
yeah, 现在只需要一步就reduce出来结果了, reduce应用 map(inc)(sum)
来计算值, 只需要遍历一遍coll
Reducer
但是如果我们不想改变map函数的接口, 原始形式的接口还是比较好写好读的
reduce(sum, 0, map(inc [1,2,3]))
那么需要进一步的抽象, 我把新的map函数叫做rmap好了
function rmap(fn, coll){
reducer(coll, map(fn))
}
跟以前接口一样,接收函数和coll,但是返回一个由reducer生成的reducible, 所以就变成了
reduce(sum, 0, reducer([1,2,3], map(inc)))
等等,怎么做到的...你已经消费了coll了, 那reducing函数怎么进来的, reducer怎么知道用sum去reduce呢.
Reducible
答案是, 反转reduce的关系, 原来reduce用sum去计算结果, 现在,我们调用reducible的reduce方法来计算结果
如果你还没有被我弄晕的话, 准备好, 又来一个新单词 reducible. 也就是可以被reduce的东西.
于是我们需要coll实现reduce方法,这样就成为reducible了.
也就是reduce函数现在应该长这样, 我们暂且叫它 rreduce
function rreduce(reduceFn, init, reducible){
reducible(reduceFn, init)
}
那么我们的例子就变成了这样
reducer([1,2,3], map(inc))(sum, 0)
reducer接收coll和xf, 返回reducible函数. 这一切都是lazy的, 直到rreduce调用第(coll)行才执行.
function reducer(coll, xf){
return function(reduceFn, init){
return coll.reduce(xf(reduceFn), init) (ref:coll)
}
}
Transducer
说了半天Reducer,明明说好的要解释的Transducer呢?
如果你还能follow, 那么现在要开始解释Transducer了
其实你已经见过Transducer了, 再回顾一下之前说的Reducer
- 接收一个xf函数和一个coll
- 用xf转换reducing函数, 并应用到coll
Transducer就是那个xf
reduce(map(inc)(sum),0,[1,2,3])
也就是这里面的 map(inc)
靠, 就这么简单?
就是这么简单, 前面说了reducer的出现是因为想保持原始reduce的api不便, 那么tranducer则提供了 另外一种reduce api
transduce(map(inc), sum, 0, [1,2,3])
transduce接收一个transducer,一个reducing function, 一个初始值, 一个coll. 这段代码跟前面干的事情一模一样.
另外牛逼的是transducer跟context完全没有关系, 就是完全与数据解耦开来, 比如我们组装好一个transducer xf
可以用在任何地方
seq(xf data) //生成一个lazy的序列, 同时lazy transform, 每次取的时候data会被transform
into([], xf data) //把 data transform后放到一个数组里
chan(1, xform) // 当数据经过CSP的channel时被transform
Is it Curry?
怎么看着有点像柯里化, 一样么?
当然不是, 柯里化或者部分参数只是部分配置参数, 而transducer是一次多n次转换的组合
比如一个柯里化的map可以
var mapinc = map(inc)
mapinc([1,2,3])
而不能
mapinc(sum)
因为map就俩参数, 第一个是函数第二个是data, 如果再给data会错误
但是tranceducer只是转换, 所以只接受reducing函数
reduce(mapinc(sum), 0, [1,2,3])
// => 9
完整例子
[1] Clojure的作者
[2] 我是故意吧reduce的参数顺序写"反"的, 原来underscore是先消费collection的. 至于为什么要反过来 可以参考这个解释
[3] clojurescript作者把clojurescript的一些数据结构和函数编译成javascript, 这样就可以用普通js使用 clojure中的数据结构和函数. document严重过时, 建议看导入的源代码, 以及clojure的文档, 接口和clojure基本一致.