前言

在我的上一篇文章中,用两种不同的方法实现了 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 组合的含义,但它还要更好:组合完成后的管道仍然是一个通道,我们可以很容易地重新组合它。


robertluo
738 声望21 粉丝

引用和评论

0 条评论