通过上一篇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:

  1. reducing 函数: 用来reduce的函数, 比如sum
  2. transform: 变换, 从一个函数变另一个函数
  3. xf: xform, transform 函数
  4. reducible: 可被reduce的,也就是实现reduce接口的,比如所有的collection

让我们一步一步分析一下这次reduce到底干了什么

  1. map 函数 inc 到 coll 每一个元素, 得到一个新的 coll [2,3,4]
  2. 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

  1. 接收一个xf函数和一个coll
  2. 用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基本一致.