前言
在我的上一篇文章中,用两种不同的方法实现了 transformer 函数到 actor。其中 pipe
版本明显更加简单。这引发了我的进一步思考。
显然,actor 本身实现中用函数来进行循环与 transducer 的思想高度一致。实际上,两者都通过封装状态来实现了纯函数化的外在表现。由于 transducer/transformer 在 clojure 中已经被实现为通用的模式,我们是否还有必要来使用 actor 呢?
actor 与 channel 的区别
在 clojure 的一个 actor 库实现,pulsar 的文档中, 作者阐述了在程序员用户角度来看的 actor 与 channel 的区别:
channel 更像是水管,里面流动的是相同形式的数据,将这些数据送往程序不同部分;而 actor 更像是接线板,它支持各种不同的连接方式(消息)。
从这个定义来看,actor 实在是一种面向对象的观念,它可以直接被视为是异步对象。这是为什么在 erlang 或 pulsar 中,actor 从实现上就与模式匹配(pattern matching)紧紧连接在了一起。我们甚至可以将每种不同的消息看做是对象的不同方法(Java 中的method)。
但我们真的需要这么返回到面向对象的领域吗?将我们的函数重新组织成方法是否是合理的思路?
主动通道
仔细看看 core.async 中 pipe
, pub
等函数的实现,我发现通道本身就是一个可以容纳主动处理过程的数据结构,我们没有显著的必要再引入新概念来完成它!它本身包含输入(ReadPort)和输出(WritePort)两端,多么象 Unix 文件啊。唯一的麻烦是,core.async 本身没有提供函数来方便地将通道进行组合。因此,下面几个函数:
(require '[clojure.core.async :as a])
(defn attach
"将 ch-input 和 ch-output 两个通道连接成为一个新的通道:
写这个通道将放进 ch-input, ch-input 则会主动将值写进 ch-output, 读取这个通道将获得 ch-output 的值."
[ch-input ch-output]
(reify
p/ReadPort
(take! [_ f]
(p/take! ch-output f))
p/WritePort
(put! [_ v f]
(p/put! ch-input v f))
p/Channel
(close! [_]
(p/close! ch-input))
(closed? [_]
(p/closed? ch-input))))
(def xf-chan
"使用 transform 函数创建一个主动通道, 写入它的数据会被 xf 转换, 可以在从它读出"
(partial a/chan (a/dropping-buffer 32)))
(defn | [& chs]
(reduce (fn [rst cur] (a/pipe rst cur) (attach rst cur)) chs))
(defn ac-prn
"一个简单的打印过渡主动通道, 一般用于开发调试"
[& [name]]
(xf-chan
(map
(fn [x]
(-> (or name "output") (str ":" x) println)
x))))
可以自由地将 channel 进行拼装组合。例如:
(def ac1 (| (a/to-chan [1 2 3]) (xf-chan (map inc)) (ac-prn)))
;会直接打印出 2, 3, 4
(a/<!! ac1)
; 2
连接函数|
很好地模仿了 Unix 组合的含义,但它还要更好:组合完成后的管道仍然是一个通道,我们可以很容易地重新组合它。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。